Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/lib/drivers/kolab/plugins/libkolab/composer.json b/lib/drivers/kolab/plugins/kolab_auth/composer.json
similarity index 71%
copy from lib/drivers/kolab/plugins/libkolab/composer.json
copy to lib/drivers/kolab/plugins/kolab_auth/composer.json
index 8926037..3e7012f 100644
--- a/lib/drivers/kolab/plugins/libkolab/composer.json
+++ b/lib/drivers/kolab/plugins/kolab_auth/composer.json
@@ -1,30 +1,30 @@
{
- "name": "kolab/libkolab",
+ "name": "kolab/kolab_auth",
"type": "roundcube-plugin",
- "description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
+ "description": "Kolab authentication",
"homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
"license": "AGPLv3",
- "version": "1.1.0",
+ "version": "3.2.2",
"authors": [
{
"name": "Thomas Bruederli",
"email": "bruederli@kolabsys.com",
"role": "Lead"
},
{
- "name": "Alensader Machniak",
+ "name": "Aleksander Machniak",
"email": "machniak@kolabsys.com",
- "role": "Developer"
+ "role": "Lead"
}
],
"repositories": [
{
"type": "composer",
"url": "http://plugins.roundcube.net"
}
],
"require": {
"php": ">=5.3.0",
"roundcube/plugin-installer": ">=0.1.3"
}
}
diff --git a/lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist b/lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist
index e7b9d15..8c01d56 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist
+++ b/lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist
@@ -1,80 +1,91 @@
<?php
// The id of the LDAP address book (which refers to the $rcmail_config['ldap_public'])
// or complete addressbook definition array.
// --------------------------------------------------------------------
// Note: Multi-domain (hosted) installations can resolve domain aliases
// by adding following settings in kolab_auth_addressbook spec.:
//
// 'domain_base_dn' => 'cn=kolab,cn=config',
// 'domain_filter' => '(&(objectclass=domainrelatedobject)(associateddomain=%s))',
// 'domain_name_attr' => 'associateddomain',
//
// With this %dc variable in base_dn and groups/base_dn will be
// replaced with DN string of resolved domain
//---------------------------------------------------------------------
-$rcmail_config['kolab_auth_addressbook'] = '';
+$config['kolab_auth_addressbook'] = '';
// This will overwrite defined filter
-$rcmail_config['kolab_auth_filter'] = '(&(objectClass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)(alias=%fu)))';
+$config['kolab_auth_filter'] = '(&(objectClass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)(alias=%fu)))';
-// Use this fields (from fieldmap configuration) to get authentication ID
-$rcmail_config['kolab_auth_login'] = 'email';
+// Use this field (from fieldmap configuration) to get authentication ID. Don't use an array here!
+$config['kolab_auth_login'] = 'email';
-// Use this fields (from fieldmap configuration) for default identity.
+// Use these fields (from fieldmap configuration) for default identity.
// If the value array contains more than one field, first non-empty will be used
// Note: These aren't LDAP attributes, but field names in config
// Note: If there's more than one email address, as many identities will be created
-$rcmail_config['kolab_auth_name'] = array('name', 'cn');
-$rcmail_config['kolab_auth_email'] = array('email');
-$rcmail_config['kolab_auth_organization'] = array('organization');
+$config['kolab_auth_name'] = array('name', 'cn');
+$config['kolab_auth_email'] = array('email');
+$config['kolab_auth_organization'] = array('organization');
+
+// Role field (from fieldmap configuration)
+$config['kolab_auth_role'] = 'role';
+
+// Template for user names displayed in the UI.
+// You can use all attributes from the 'fieldmap' property of the 'kolab_auth_addressbook' configuration
+$config['kolab_auth_user_displayname'] = '{name} ({ou})';
// Login and password of the admin user. Enables "Login As" feature.
-$rcmail_config['kolab_auth_admin_login'] = '';
-$rcmail_config['kolab_auth_admin_password'] = '';
+$config['kolab_auth_admin_login'] = '';
+$config['kolab_auth_admin_password'] = '';
// Enable audit logging for abuse of administrative privileges.
-$rcmail_config['kolab_auth_auditlog'] = true;
-
-// Role field (from fieldmap configuration)
-$rcmail_config['kolab_auth_role'] = 'role';
-// The required value for the role attribute to contain should the user be allowed
-// to login as another user.
-$rcmail_config['kolab_auth_role_value'] = '';
+$config['kolab_auth_auditlog'] = false;
-// Administrative group name to which user must be assigned to
-// which adds privilege to login as another user.
-$rcmail_config['kolab_auth_group'] = '';
+// As set of rules to define the required rights on the target entry
+// which allow an admin user to login as another user (the target).
+// The effective rights value refers to either entry level attribute level rights:
+// * entry:[read|add|delete]
+// * attrib:<attribute-name>:[read|write|delete]
+$config['kolab_auth_admin_rights'] = array(
+ // Roundcube task => required effective right
+ 'settings' => 'entry:read',
+ 'mail' => 'entry:delete',
+ 'addressbook' => 'entry:delete',
+ // or use a wildcard entry like this:
+ '*' => 'entry:read',
+);
// Enable plugins on a role-by-role basis. In this example, the 'acl' plugin
// is enabled for people with a 'cn=professional-user,dc=mykolab,dc=ch' role.
//
// Note that this does NOT mean the 'acl' plugin is disabled for other people.
-$rcmail_config['kolab_auth_role_plugins'] = Array(
+$config['kolab_auth_role_plugins'] = Array(
'cn=professional-user,dc=mykolab,dc=ch' => Array(
'acl',
),
);
// Settings on a role-by-role basis. In this example, the 'htmleditor' setting
// is enabled(1) for people with a 'cn=professional-user,dc=mykolab,dc=ch' role,
// and it cannot be overridden. Sample use-case: disable htmleditor for normal people,
// do not allow the setting to be controlled through the preferences, enable the
// html editor for professional users and allow them to override the setting in
// the preferences.
-$rcmail_config['kolab_auth_role_settings'] = Array(
+$config['kolab_auth_role_settings'] = Array(
'cn=professional-user,dc=mykolab,dc=ch' => Array(
'htmleditor' => Array(
'mode' => 'override',
'value' => 1,
'allow_override' => true
),
),
);
// List of LDAP addressbooks (keys of ldap_public configuration array)
// for which base_dn variables (%dc, etc.) will be replaced according to authenticated user DN
// Note: special name '*' for all LDAP addressbooks
-$rcmail_config['kolab_auth_ldap_addressbooks'] = array('*');
+$config['kolab_auth_ldap_addressbooks'] = array('*');
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php
index 7ff5761..033d5b1 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php
+++ b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php
@@ -1,662 +1,788 @@
<?php
/**
* Kolab Authentication (based on ldap_authentication plugin)
*
* Authenticates on LDAP server, finds canonized authentication ID for IMAP
* and for new users creates identity based on LDAP information.
*
* Supports impersonate feature (login as another user). To use this feature
* imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
*
* @version @package_version@
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_auth extends rcube_plugin
{
static $ldap;
+ private $username;
private $data = array();
public function init()
{
$rcmail = rcube::get_instance();
$this->load_config();
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('user_create', array($this, 'user_create'));
// Hook for password change
$this->add_hook('password_ldap_bind', array($this, 'password_ldap_bind'));
// Hooks related to "Login As" feature
$this->add_hook('template_object_loginform', array($this, 'login_form'));
$this->add_hook('storage_connect', array($this, 'imap_connect'));
$this->add_hook('managesieve_connect', array($this, 'imap_connect'));
$this->add_hook('smtp_connect', array($this, 'smtp_connect'));
$this->add_hook('identity_form', array($this, 'identity_form'));
// Hook to modify some configuration, e.g. ldap
$this->add_hook('config_get', array($this, 'config_get'));
- // Enable debug logs per-user, this enables logging only after
- // user has logged in
- if (!empty($_SESSION['username']) && $rcmail->config->get('kolab_auth_auditlog')) {
- $this->add_hook('write_log', array($this, 'write_log'));
+ // Hook to modify logging directory
+ $this->add_hook('write_log', array($this, 'write_log'));
+ $this->username = $_SESSION['username'];
+ // Enable debug logs (per-user), when logged as another user
+ if (!empty($_SESSION['kolab_auth_admin']) && $rcmail->config->get('kolab_auth_auditlog')) {
$rcmail->config->set('debug_level', 1);
$rcmail->config->set('devel_mode', true);
$rcmail->config->set('smtp_log', true);
$rcmail->config->set('log_logins', true);
$rcmail->config->set('log_session', true);
$rcmail->config->set('memcache_debug', true);
$rcmail->config->set('imap_debug', true);
$rcmail->config->set('ldap_debug', true);
$rcmail->config->set('smtp_debug', true);
$rcmail->config->set('sql_debug', true);
// SQL debug need to be set directly on DB object
// setting config variable will not work here because
// the object is already initialized/configured
if ($db = $rcmail->get_dbh()) {
$db->set_debug(true);
}
}
}
+ /**
+ * Startup hook handler
+ */
public function startup($args)
{
+ // Check access rights when logged in as another user
+ if (!empty($_SESSION['kolab_auth_admin']) && $args['task'] != 'login' && $args['task'] != 'logout') {
+ // access to specified task is forbidden,
+ // redirect to the first task on the list
+ if (!empty($_SESSION['kolab_auth_allowed_tasks'])) {
+ $tasks = (array)$_SESSION['kolab_auth_allowed_tasks'];
+ if (!in_array($args['task'], $tasks) && !in_array('*', $tasks)) {
+ header('Location: ?_task=' . array_shift($tasks));
+ die;
+ }
+
+ // add script that will remove disabled taskbar buttons
+ if (!in_array('*', $tasks)) {
+ $this->add_hook('render_page', array($this, 'render_page'));
+ }
+ }
+ }
+
+ // load per-user settings
$this->load_user_role_plugins_and_settings();
return $args;
}
/**
* Modify some configuration according to LDAP user record
*/
public function config_get($args)
{
// Replaces ldap_vars (%dc, etc) in public kolab ldap addressbooks
// config based on the users base_dn. (for multi domain support)
if ($args['name'] == 'ldap_public' && !empty($args['result'])) {
$rcmail = rcube::get_instance();
$kolab_books = (array) $rcmail->config->get('kolab_auth_ldap_addressbooks');
foreach ($args['result'] as $name => $config) {
if (in_array($name, $kolab_books) || in_array('*', $kolab_books)) {
- $args['result'][$name]['base_dn'] = self::parse_ldap_vars($config['base_dn']);
- $args['result'][$name]['search_base_dn'] = self::parse_ldap_vars($config['search_base_dn']);
- $args['result'][$name]['bind_dn'] = str_replace('%dn', $_SESSION['kolab_dn'], $config['bind_dn']);
-
- if (!empty($config['groups'])) {
- $args['result'][$name]['groups']['base_dn'] = self::parse_ldap_vars($config['groups']['base_dn']);
- }
+ $args['result'][$name] = $this->patch_ldap_config($config);
}
}
}
+ else if ($args['name'] == 'kolab_users_directory' && !empty($args['result'])) {
+ $args['result'] = $this->patch_ldap_config($args['result']);
+ }
return $args;
}
+ /**
+ * Helper method to patch the given LDAP directory config with user-specific values
+ */
+ protected function patch_ldap_config($config)
+ {
+ if (is_array($config)) {
+ $config['base_dn'] = self::parse_ldap_vars($config['base_dn']);
+ $config['search_base_dn'] = self::parse_ldap_vars($config['search_base_dn']);
+ $config['bind_dn'] = str_replace('%dn', $_SESSION['kolab_dn'], $config['bind_dn']);
+
+ if (!empty($config['groups'])) {
+ $config['groups']['base_dn'] = self::parse_ldap_vars($config['groups']['base_dn']);
+ }
+ }
+
+ return $config;
+ }
+
/**
* Modifies list of plugins and settings according to
* specified LDAP roles
*/
public function load_user_role_plugins_and_settings()
{
if (empty($_SESSION['user_roledns'])) {
return;
}
$rcmail = rcube::get_instance();
// Example 'kolab_auth_role_plugins' =
//
// Array(
// '<role_dn>' => Array('plugin1', 'plugin2'),
// );
//
// NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
$role_plugins = $rcmail->config->get('kolab_auth_role_plugins');
// Example $rcmail_config['kolab_auth_role_settings'] =
//
// Array(
// '<role_dn>' => Array(
// '$setting' => Array(
// 'mode' => '(override|merge)', (default: override)
// 'value' => <>,
// 'allow_override' => (true|false) (default: false)
// ),
// ),
// );
//
// NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
$role_settings = $rcmail->config->get('kolab_auth_role_settings');
if (!empty($role_plugins)) {
foreach ($role_plugins as $role_dn => $plugins) {
- $role_plugins[self::parse_ldap_vars($role_dn)] = $plugins;
+ $role_dn = self::parse_ldap_vars($role_dn);
+ if (!empty($role_plugins[$role_dn])) {
+ $role_plugins[$role_dn] = array_unique(array_merge((array)$role_plugins[$role_dn], $plugins));
+ } else {
+ $role_plugins[$role_dn] = $plugins;
+ }
}
}
if (!empty($role_settings)) {
foreach ($role_settings as $role_dn => $settings) {
- $role_settings[self::parse_ldap_vars($role_dn)] = $settings;
+ $role_dn = self::parse_ldap_vars($role_dn);
+ if (!empty($role_settings[$role_dn])) {
+ $role_settings[$role_dn] = array_merge((array)$role_settings[$role_dn], $settings);
+ } else {
+ $role_settings[$role_dn] = $settings;
+ }
}
}
foreach ($_SESSION['user_roledns'] as $role_dn) {
- if (isset($role_plugins[$role_dn]) && is_array($role_plugins[$role_dn])) {
- foreach ($role_plugins[$role_dn] as $plugin) {
- $this->require_plugin($plugin);
- }
- }
-
- if (isset($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
+ if (!empty($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
foreach ($role_settings[$role_dn] as $setting_name => $setting) {
if (!isset($setting['mode'])) {
$setting['mode'] = 'override';
}
if ($setting['mode'] == "override") {
$rcmail->config->set($setting_name, $setting['value']);
} elseif ($setting['mode'] == "merge") {
$orig_setting = $rcmail->config->get($setting_name);
if (!empty($orig_setting)) {
if (is_array($orig_setting)) {
$rcmail->config->set($setting_name, array_merge($orig_setting, $setting['value']));
}
} else {
$rcmail->config->set($setting_name, $setting['value']);
}
}
$dont_override = (array) $rcmail->config->get('dont_override');
- if (!isset($setting['allow_override']) || !$setting['allow_override']) {
+ if (empty($setting['allow_override'])) {
$rcmail->config->set('dont_override', array_merge($dont_override, array($setting_name)));
}
else {
if (in_array($setting_name, $dont_override)) {
$_dont_override = array();
foreach ($dont_override as $_setting) {
if ($_setting != $setting_name) {
$_dont_override[] = $_setting;
}
}
$rcmail->config->set('dont_override', $_dont_override);
}
}
+
+ if ($setting_name == 'skin') {
+ if ($rcmail->output->type == 'html') {
+ $rcmail->output->set_skin($setting['value']);
+ $rcmail->output->set_env('skin', $setting['value']);
+ }
+ }
+ }
+ }
+
+ if (!empty($role_plugins[$role_dn])) {
+ foreach ((array)$role_plugins[$role_dn] as $plugin) {
+ $this->api->load_plugin($plugin);
}
}
}
}
/**
* Logging method replacement to print debug/errors into
* a separate (sub)folder for each user
*/
public function write_log($args)
{
$rcmail = rcube::get_instance();
if ($rcmail->config->get('log_driver') == 'syslog') {
return $args;
}
- $line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
-
// log_driver == 'file' is assumed here
$log_dir = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
- $log_path = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
- // Append original username + target username
- if (!is_dir($log_path)) {
+ // Append original username + target username for audit-logging
+ if ($rcmail->config->get('kolab_auth_auditlog') && !empty($_SESSION['kolab_auth_admin'])) {
+ $args['dir'] = $log_dir . '/' . strtolower($_SESSION['kolab_auth_admin']) . '/' . strtolower($this->username);
+
// Attempt to create the directory
- if (@mkdir($log_path, 0750, true)) {
- $log_dir = $log_path;
+ if (!is_dir($args['dir'])) {
+ @mkdir($args['dir'], 0750, true);
}
}
- else {
- $log_dir = $log_path;
- }
-
- // try to open specific log file for writing
- $logfile = $log_dir.'/'.$args['name'];
-
- if ($fp = fopen($logfile, 'a')) {
- fwrite($fp, $line);
- fflush($fp);
- fclose($fp);
- }
- else {
- trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
+ // Define the user log directory if a username is provided
+ else if ($rcmail->config->get('per_user_logging') && !empty($this->username)) {
+ $user_log_dir = $log_dir . '/' . strtolower($this->username);
+ if (is_writable($user_log_dir)) {
+ $args['dir'] = $user_log_dir;
+ }
+ else if ($args['name'] != 'errors') {
+ $args['abort'] = true; // don't log if unauthenticed
+ }
}
- $args['abort'] = true;
-
return $args;
}
/**
* Sets defaults for new user.
*/
public function user_create($args)
{
if (!empty($this->data['user_email'])) {
// addresses list is supported
if (array_key_exists('email_list', $args)) {
$email_list = array_unique($this->data['user_email']);
// add organization to the list
if (!empty($this->data['user_organization'])) {
foreach ($email_list as $idx => $email) {
$email_list[$idx] = array(
'organization' => $this->data['user_organization'],
'email' => $email,
);
}
}
$args['email_list'] = $email_list;
}
else {
$args['user_email'] = $this->data['user_email'][0];
}
}
if (!empty($this->data['user_name'])) {
$args['user_name'] = $this->data['user_name'];
}
return $args;
}
/**
* Modifies login form adding additional "Login As" field
*/
public function login_form($args)
{
$this->add_texts('localization/');
$rcmail = rcube::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$group = $rcmail->config->get('kolab_auth_group');
$role_attr = $rcmail->config->get('kolab_auth_role');
// Show "Login As" input
if (empty($admin_login) || (empty($group) && empty($role_attr))) {
return $args;
}
$input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
'type' => 'text', 'autocomplete' => 'off'));
$row = html::tag('tr', null,
html::tag('td', 'title', html::label('rcmloginas', Q($this->gettext('loginas'))))
. html::tag('td', 'input', $input->show(trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST))))
);
$args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>', $args['content']);
return $args;
}
/**
* Find user credentials In LDAP.
*/
public function authenticate($args)
{
// get username and host
$host = $args['host'];
$user = $args['user'];
$pass = $args['pass'];
$loginas = trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST));
if (empty($user) || empty($pass)) {
$args['abort'] = true;
return $args;
}
+ // temporarily set the current username to the one submitted
+ $this->username = $user;
+
$ldap = self::ldap();
if (!$ldap || !$ldap->ready) {
$args['abort'] = true;
+ $args['kolab_ldap_error'] = true;
$message = sprintf(
'Login failure for user %s from %s in session %s (error %s)',
$user,
rcube_utils::remote_ip(),
session_id(),
"LDAP not ready"
);
rcube::write_log('userlogins', $message);
return $args;
}
// Find user record in LDAP
$record = $ldap->get_user_record($user, $host);
if (empty($record)) {
$args['abort'] = true;
$message = sprintf(
'Login failure for user %s from %s in session %s (error %s)',
$user,
rcube_utils::remote_ip(),
session_id(),
"No user record found"
);
rcube::write_log('userlogins', $message);
return $args;
}
$rcmail = rcube::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$admin_pass = $rcmail->config->get('kolab_auth_admin_password');
$login_attr = $rcmail->config->get('kolab_auth_login');
$name_attr = $rcmail->config->get('kolab_auth_name');
$email_attr = $rcmail->config->get('kolab_auth_email');
$org_attr = $rcmail->config->get('kolab_auth_organization');
$role_attr = $rcmail->config->get('kolab_auth_role');
$imap_attr = $rcmail->config->get('kolab_auth_mailhost');
if (!empty($role_attr) && !empty($record[$role_attr])) {
$_SESSION['user_roledns'] = (array)($record[$role_attr]);
}
if (!empty($imap_attr) && !empty($record[$imap_attr])) {
$default_host = $rcmail->config->get('default_host');
if (!empty($default_host)) {
rcube::write_log("errors", "Both default host and kolab_auth_mailhost set. Incompatible.");
} else {
$args['host'] = "tls://" . $record[$imap_attr];
}
}
// Login As...
if (!empty($loginas) && $admin_login) {
// Authenticate to LDAP
$result = $ldap->bind($record['dn'], $pass);
if (!$result) {
$args['abort'] = true;
$message = sprintf(
'Login failure for user %s from %s in session %s (error %s)',
$user,
rcube_utils::remote_ip(),
session_id(),
"Unable to bind with '" . $record['dn'] . "'"
);
rcube::write_log('userlogins', $message);
return $args;
}
- // check if the original user has/belongs to administrative role/group
$isadmin = false;
- $group = $rcmail->config->get('kolab_auth_group');
- $role_dn = $rcmail->config->get('kolab_auth_role_value');
-
- // check role attribute
- if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
- $role_dn = $ldap->parse_vars($role_dn, $user, $host);
- if (in_array($role_dn, (array)$record[$role_attr])) {
- $isadmin = true;
+ $admin_rights = $rcmail->config->get('kolab_auth_admin_rights', array());
+
+ // @deprecated: fall-back to the old check if the original user has/belongs to administrative role/group
+ if (empty($admin_rights)) {
+ $group = $rcmail->config->get('kolab_auth_group');
+ $role_dn = $rcmail->config->get('kolab_auth_role_value');
+
+ // check role attribute
+ if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
+ $role_dn = $ldap->parse_vars($role_dn, $user, $host);
+ if (in_array($role_dn, (array)$record[$role_attr])) {
+ $isadmin = true;
+ }
+ }
+
+ // check group
+ if (!$isadmin && !empty($group)) {
+ $groups = $ldap->get_user_groups($record['dn'], $user, $host);
+ if (in_array($group, $groups)) {
+ $isadmin = true;
+ }
+ }
+
+ if ($isadmin) {
+ // user has admin privileges privilage, get "login as" user credentials
+ $target_entry = $ldap->get_user_record($loginas, $host);
+ $allowed_tasks = $rcmail->config->get('kolab_auth_allowed_tasks');
}
}
+ else {
+ // get "login as" user credentials
+ $target_entry = $ldap->get_user_record($loginas, $host);
+
+ if (!empty($target_entry)) {
+ // get effective rights to determine login-as permissions
+ $effective_rights = (array)$ldap->effective_rights($target_entry['dn']);
+
+ if (!empty($effective_rights)) {
+ $effective_rights['attrib'] = $effective_rights['attributeLevelRights'];
+ $effective_rights['entry'] = $effective_rights['entryLevelRights'];
+
+ // compare the rights with the permissions mapping
+ $allowed_tasks = array();
+ foreach ($admin_rights as $task => $perms) {
+ $perms_ = explode(':', $perms);
+ $type = array_shift($perms_);
+ $req = array_pop($perms_);
+ $attrib = array_pop($perms_);
+
+ if (array_key_exists($type, $effective_rights)) {
+ if ($type == 'entry' && in_array($req, $effective_rights[$type])) {
+ $allowed_tasks[] = $task;
+ }
+ else if ($type == 'attrib' && array_key_exists($attrib, $effective_rights[$type]) &&
+ in_array($req, $effective_rights[$type][$attrib])) {
+ $allowed_tasks[] = $task;
+ }
+ }
+ }
- // check group
- if (!$isadmin && !empty($group)) {
- $groups = $ldap->get_user_groups($record['dn'], $user, $host);
- if (in_array($group, $groups)) {
- $isadmin = true;
+ $isadmin = !empty($allowed_tasks);
+ }
}
}
// Save original user login for log (see below)
if ($login_attr) {
$origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
}
else {
$origname = $user;
}
- $record = null;
-
- // user has the privilage, get "login as" user credentials
- if ($isadmin) {
- $record = $ldap->get_user_record($loginas, $host);
- }
+ if (!$isadmin || empty($target_entry)) {
+ $this->add_texts('localization/');
- if (empty($record)) {
$args['abort'] = true;
+ $args['error'] = $this->gettext(array(
+ 'name' => 'loginasnotallowed',
+ 'vars' => array('user' => Q($loginas)),
+ ));
+
$message = sprintf(
'Login failure for user %s (as user %s) from %s in session %s (error %s)',
$user,
$loginas,
rcube_utils::remote_ip(),
session_id(),
- "No user record found for '" . $loginas . "'"
+ "No privileges to login as '" . $loginas . "'"
);
rcube::write_log('userlogins', $message);
return $args;
}
- $args['user'] = $loginas;
+ // replace $record with target entry
+ $record = $target_entry;
+
+ $args['user'] = $this->username = $loginas;
// Mark session to use SASL proxy for IMAP authentication
$_SESSION['kolab_auth_admin'] = strtolower($origname);
$_SESSION['kolab_auth_login'] = $rcmail->encrypt($admin_login);
$_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
+ $_SESSION['kolab_auth_allowed_tasks'] = $allowed_tasks;
}
// Store UID and DN of logged user in session for use by other plugins
$_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
$_SESSION['kolab_dn'] = $record['dn'];
// Store LDAP replacement variables used for current user
// This improves performance of load_user_role_plugins_and_settings()
// which is executed on every request (via startup hook) and where
// we don't like to use LDAP (connection + bind + search)
$_SESSION['kolab_auth_vars'] = $ldap->get_parse_vars();
// Set user login
if ($login_attr) {
$this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
}
if ($this->data['user_login']) {
- $args['user'] = $this->data['user_login'];
+ $args['user'] = $this->username = $this->data['user_login'];
}
// User name for identity (first log in)
foreach ((array)$name_attr as $field) {
$name = is_array($record[$field]) ? $record[$field][0] : $record[$field];
if (!empty($name)) {
$this->data['user_name'] = $name;
break;
}
}
// User email(s) for identity (first log in)
foreach ((array)$email_attr as $field) {
$email = is_array($record[$field]) ? array_filter($record[$field]) : $record[$field];
if (!empty($email)) {
$this->data['user_email'] = array_merge((array)$this->data['user_email'], (array)$email);
}
}
// Organization name for identity (first log in)
foreach ((array)$org_attr as $field) {
$organization = is_array($record[$field]) ? $record[$field][0] : $record[$field];
if (!empty($organization)) {
$this->data['user_organization'] = $organization;
break;
}
}
// Log "Login As" usage
if (!empty($origname)) {
rcube::write_log('userlogins', sprintf('Admin login for %s by %s from %s',
$args['user'], $origname, rcube_utils::remote_ip()));
}
+ // load per-user settings/plugins
+ $this->load_user_role_plugins_and_settings();
+
return $args;
}
/**
* Set user DN for password change (password plugin with ldap_simple driver)
*/
public function password_ldap_bind($args)
{
$args['user_dn'] = $_SESSION['kolab_dn'];
$rcmail = rcube::get_instance();
$rcmail->config->set('password_ldap_method', 'user');
return $args;
}
/**
* Sets SASL Proxy login/password for IMAP and Managesieve auth
*/
public function imap_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcube::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['auth_cid'] = $admin_login;
$args['auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Sets SASL Proxy login/password for SMTP auth
*/
public function smtp_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcube::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
- $args['options']['smtp_auth_cid'] = $admin_login;
- $args['options']['smtp_auth_pw'] = $admin_pass;
+ $args['smtp_auth_cid'] = $admin_login;
+ $args['smtp_auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Hook to replace the plain text input field for email address by a drop-down list
* with all email addresses (including aliases) from this user's LDAP record.
*/
public function identity_form($args)
{
$rcmail = rcube::get_instance();
$ident_level = intval($rcmail->config->get('identities_level', 0));
// do nothing if email address modification is disabled
if ($ident_level == 1 || $ident_level == 3) {
return $args;
}
$ldap = self::ldap();
if (!$ldap || !$ldap->ready || empty($_SESSION['kolab_dn'])) {
return $args;
}
$emails = array();
$user_record = $ldap->get_record($_SESSION['kolab_dn']);
foreach ((array)$rcmail->config->get('kolab_auth_email', array()) as $col) {
$values = rcube_addressbook::get_col_values($col, $user_record, true);
if (!empty($values))
$emails = array_merge($emails, array_filter($values));
}
// kolab_delegation might want to modify this addresses list
$plugin = $rcmail->plugins->exec_hook('kolab_auth_emails', array('emails' => $emails));
$emails = $plugin['emails'];
if (!empty($emails)) {
$args['form']['addressing']['content']['email'] = array(
'type' => 'select',
'options' => array_combine($emails, $emails),
);
}
return $args;
}
+ /**
+ * Action executed before the page is rendered to add an onload script
+ * that will remove all taskbar buttons for disabled tasks
+ */
+ public function render_page($args)
+ {
+ $rcmail = rcube::get_instance();
+ $tasks = (array)$_SESSION['kolab_auth_allowed_tasks'];
+ $tasks[] = 'logout';
+
+ // disable buttons in taskbar
+ $script = "
+ \$('a').filter(function() {
+ var ev = \$(this).attr('onclick');
+ return ev && ev.match(/'switch-task','([a-z]+)'/)
+ && \$.inArray(RegExp.\$1, " . json_encode($tasks) . ") < 0;
+ }).remove();
+ ";
+
+ $rcmail->output->add_script($script, 'docready');
+ }
+
/**
* Initializes LDAP object and connects to LDAP server
*/
public static function ldap()
{
if (self::$ldap) {
return self::$ldap;
}
$rcmail = rcube::get_instance();
$addressbook = $rcmail->config->get('kolab_auth_addressbook');
if (!is_array($addressbook)) {
$ldap_config = (array)$rcmail->config->get('ldap_public');
$addressbook = $ldap_config[$addressbook];
}
if (empty($addressbook)) {
return null;
}
require_once __DIR__ . '/kolab_auth_ldap.php';
self::$ldap = new kolab_auth_ldap($addressbook);
return self::$ldap;
}
/**
* Parses LDAP DN string with replacing supported variables.
* See kolab_auth_ldap::parse_vars()
*
* @param string $str LDAP DN string
*
* @return string Parsed DN string
*/
public static function parse_ldap_vars($str)
{
if (!empty($_SESSION['kolab_auth_vars'])) {
$str = strtr($str, $_SESSION['kolab_auth_vars']);
}
return $str;
}
}
diff --git a/lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php
index b9b3e4a..431133b 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php
@@ -1,499 +1,548 @@
<?php
/**
* Kolab Authentication
*
* @version @package_version@
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Wrapper class for rcube_ldap_generic
*/
class kolab_auth_ldap extends rcube_ldap_generic
{
private $icache = array();
+ private $conf = array();
+ private $fieldmap = array();
function __construct($p)
{
$rcmail = rcube::get_instance();
- $this->debug = (bool) $rcmail->config->get('ldap_debug');
+ $this->conf = $p;
+ $this->conf['kolab_auth_user_displayname'] = $rcmail->config->get('kolab_auth_user_displayname', '{name}');
+
$this->fieldmap = $p['fieldmap'];
$this->fieldmap['uid'] = 'uid';
$p['attributes'] = array_values($this->fieldmap);
+ $p['debug'] = (bool) $rcmail->config->get('ldap_debug');
// Connect to the server (with bind)
parent::__construct($p);
$this->_connect();
$rcmail->add_shutdown_function(array($this, 'close'));
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
- $rcube = rcube::get_instance();
-
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ((array)$this->config['hosts'] as $host) {
// skip host if connection failed
if (!$this->connect($host)) {
continue;
}
$bind_pass = $this->config['bind_pass'];
$bind_user = $this->config['bind_user'];
$bind_dn = $this->config['bind_dn'];
if (empty($bind_pass)) {
$this->ready = true;
}
else {
if (!empty($bind_dn)) {
$this->ready = $this->bind($bind_dn, $bind_pass);
}
else if (!empty($this->config['auth_cid'])) {
$this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_user);
}
else {
$this->ready = $this->sasl_bind($bind_user, $bind_pass);
}
}
// connection established, we're done here
if ($this->ready) {
break;
}
} // end foreach hosts
if (!is_resource($this->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to any LDAP server, last tried $host"), true);
$this->ready = false;
}
return $this->ready;
}
/**
* Fetches user data from LDAP addressbook
*/
function get_user_record($user, $host)
{
$rcmail = rcube::get_instance();
$filter = $rcmail->config->get('kolab_auth_filter');
$filter = $this->parse_vars($filter, $user, $host);
$base_dn = $this->parse_vars($this->config['base_dn'], $user, $host);
$scope = $this->config['scope'];
// @TODO: print error if filter is empty
// get record
if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) {
if ($result->count() == 1) {
$entries = $result->entries(true);
$dn = key($entries);
$entry = array_pop($entries);
$entry = $this->field_mapping($dn, $entry);
return $entry;
}
}
}
/**
* Fetches user data from LDAP addressbook
*/
function get_user_groups($dn, $user, $host)
{
if (empty($dn) || empty($this->config['groups'])) {
return array();
}
$base_dn = $this->parse_vars($this->config['groups']['base_dn'], $user, $host);
$name_attr = $this->config['groups']['name_attr'] ? $this->config['groups']['name_attr'] : 'cn';
$member_attr = $this->get_group_member_attr();
$filter = "(member=$dn)(uniqueMember=$dn)";
if ($member_attr != 'member' && $member_attr != 'uniqueMember')
$filter .= "($member_attr=$dn)";
$filter = strtr("(|$filter)", array("\\" => "\\\\"));
$result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr));
if (!$result) {
return array();
}
$groups = array();
foreach ($result as $entry) {
+ $dn = $entry['dn'];
$entry = rcube_ldap_generic::normalize_entry($entry);
- if (!$entry['dn']) {
- $entry['dn'] = $result->get_dn();
- }
- $groups[$entry['dn']] = $entry[$name_attr];
+
+ $groups[$dn] = $entry[$name_attr];
}
return $groups;
}
/**
* Get a specific LDAP record
*
* @param string DN
*
* @return array Record data
*/
function get_record($dn)
{
if (!$this->ready) {
return;
}
if ($rec = $this->get_entry($dn)) {
$rec = rcube_ldap_generic::normalize_entry($rec);
$rec = $this->field_mapping($dn, $rec);
}
return $rec;
}
/**
* Replace LDAP record data items
*
* @param string $dn DN
* @param array $entry LDAP entry
*
* return bool True on success, False on failure
*/
function replace($dn, $entry)
{
// fields mapping
foreach ($this->fieldmap as $field => $attr) {
if (array_key_exists($field, $entry)) {
$entry[$attr] = $entry[$field];
- unset($entry[$field]);
+ if ($attr != $field) {
+ unset($entry[$field]);
+ }
}
}
return $this->mod_replace($dn, $entry);
}
/**
* Search records (simplified version of rcube_ldap::search)
*
- * @param mixed $fields The field name of array of field names to search in
+ * @param mixed $fields The field name or array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
- * @param boolean $select True if results are requested, False if count only
* @param array $required List of fields that cannot be empty
* @param int $limit Number of records
+ * @param int $count Returns the number of records found
*
* @return array List or false on error
*/
- function search($fields, $value, $mode=1, $required = array(), $limit = 0)
+ function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
{
+ if (empty($fields)) {
+ return array();
+ }
+
$mode = intval($mode);
// use AND operator for advanced searches
$filter = is_array($value) ? '(&' : '(|';
// set wildcards
$wp = $ws = '';
if (!empty($this->config['fuzzy_search']) && $mode != 1) {
$ws = '*';
if (!$mode) {
$wp = '*';
}
}
foreach ((array)$fields as $idx => $field) {
- $val = is_array($value) ? $value[$idx] : $value;
- if ($attrs = (array) $this->fieldmap[$field]) {
+ $val = is_array($value) ? $value[$idx] : $value;
+ $attrs = (array) $this->fieldmap[$field];
+
+ if (empty($attrs)) {
+ $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
+ }
+ else {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
$filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
}
$filter .= ')';
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
if (in_array($field, (array)$fields)) // required field is already in search filter
continue;
- if ($attrs = (array) $this->fieldmap[$field]) {
+
+ $attrs = (array) $this->fieldmap[$field];
+
+ if (empty($attrs)) {
+ $req_filter .= "($field=*)";
+ }
+ else {
if (count($attrs) > 1)
$req_filter .= '(|';
foreach ($attrs as $f)
$req_filter .= "($f=*)";
if (count($attrs) > 1)
$req_filter .= ')';
}
}
if (!empty($req_filter)) {
$filter = '(&' . $req_filter . $filter . ')';
}
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
// add general filter to query
if (!empty($this->config['filter'])) {
$filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config['filter']) . ')' . $filter . ')';
}
$base_dn = $this->parse_vars($this->config['base_dn']);
$scope = $this->config['scope'];
$attrs = array_values($this->fieldmap);
$list = array();
- if ($result = parent::search($base_dn, $filter, $scope, $attrs)) {
+ if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
+ $count = $result->count();
$i = 0;
foreach ($result as $entry) {
if ($limit && $limit <= $i) {
break;
}
- $dn = $result->get_dn();
+
+ $dn = $entry['dn'];
$entry = rcube_ldap_generic::normalize_entry($entry);
$list[$dn] = $this->field_mapping($dn, $entry);
$i++;
}
}
return $list;
}
/**
* Set filter used in search()
*/
function set_filter($filter)
{
$this->config['filter'] = $filter;
}
/**
* Maps LDAP attributes to defined fields
*/
protected function field_mapping($dn, $entry)
{
$entry['dn'] = $dn;
// fields mapping
foreach ($this->fieldmap as $field => $attr) {
- if (isset($entry[$attr])) {
+ // $entry might be indexed by lower-case attribute names
+ $attr_lc = strtolower($attr);
+ if (isset($entry[$attr_lc])) {
+ $entry[$field] = $entry[$attr_lc];
+ }
+ else if (isset($entry[$attr])) {
$entry[$field] = $entry[$attr];
}
}
+ // compose display name according to config
+ if (empty($this->fieldmap['displayname'])) {
+ $entry['displayname'] = rcube_addressbook::compose_search_name(
+ $entry,
+ $entry['email'],
+ $entry['name'],
+ $this->conf['kolab_auth_user_displayname']
+ );
+ }
+
return $entry;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array())
{
if (empty($object_classes)) {
$object_classes = $this->config['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
switch (strtolower($oc)) {
case 'group':
case 'groupofnames':
case 'kolabgroupofnames':
$member_attr = 'member';
break;
case 'groupofuniquenames':
case 'kolabgroupofuniquenames':
$member_attr = 'uniqueMember';
break;
}
}
}
if (!empty($member_attr)) {
return $member_attr;
}
if (!empty($this->config['groups']['member_attr'])) {
return $this->config['groups']['member_attr'];
}
return 'member';
}
/**
* Prepares filter query for LDAP search
*/
function parse_vars($str, $user = null, $host = null)
{
// When authenticating user $user is always set
// if not set it means we use this LDAP object for other
// purposes, e.g. kolab_delegation, then username with
// correct domain is in a session
if (!$user) {
$user = $_SESSION['username'];
}
if (isset($this->icache[$user])) {
list($user, $dc) = $this->icache[$user];
}
else {
$orig_user = $user;
$rcmail = rcube::get_instance();
// get default domain
if ($username_domain = $rcmail->config->get('username_domain')) {
if ($host && is_array($username_domain) && isset($username_domain[$host])) {
$domain = rcube_utils::parse_host($username_domain[$host], $host);
}
else if (is_string($username_domain)) {
$domain = rcube_utils::parse_host($username_domain, $host);
}
}
// realmed username (with domain)
if (strpos($user, '@')) {
list($usr, $dom) = explode('@', $user);
// unrealm domain, user login can contain a domain alias
if ($dom != $domain && ($dc = $this->find_domain($dom))) {
// @FIXME: we should replace domain in $user, I suppose
}
}
else if ($domain) {
$user .= '@' . $domain;
}
$this->icache[$orig_user] = array($user, $dc);
}
// replace variables in filter
list($u, $d) = explode('@', $user);
// hierarchal domain string
if (empty($dc)) {
$dc = 'dc=' . strtr($d, array('.' => ',dc='));
}
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
$this->parse_replaces = $replaces;
return strtr($str, $replaces);
}
/**
* Find root domain for specified domain
*
* @param string $domain Domain name
*
* @return string Domain DN string
*/
function find_domain($domain)
{
if (empty($domain) || empty($this->config['domain_base_dn']) || empty($this->config['domain_filter'])) {
return null;
}
$base_dn = $this->config['domain_base_dn'];
$filter = $this->config['domain_filter'];
$name_attr = $this->config['domain_name_attribute'];
if (empty($name_attr)) {
$name_attr = 'associateddomain';
}
$filter = str_replace('%s', rcube_ldap_generic::quote_string($domain), $filter);
$result = parent::search($base_dn, $filter, 'sub', array($name_attr, 'inetdomainbasedn'));
if (!$result) {
return null;
}
$entries = $result->entries(true);
$entry_dn = key($entries);
$entry = $entries[$entry_dn];
if (is_array($entry)) {
if (!empty($entry['inetdomainbasedn'])) {
return $entry['inetdomainbasedn'];
}
$domain = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
return $domain ? 'dc=' . implode(',dc=', explode('.', $domain)) : null;
}
}
/**
* Returns variables used for replacement in (last) parse_vars() call
*
* @return array Variable-value hash array
*/
public function get_parse_vars()
{
return $this->parse_replaces;
}
+ /**
+ * Register additional fields
+ */
+ public function extend_fieldmap($map)
+ {
+ foreach ((array)$map as $name => $attr) {
+ if (!in_array($attr, $this->attributes)) {
+ $this->attributes[] = $attr;
+ $this->fieldmap[$name] = $attr;
+ }
+ }
+ }
+
/**
* HTML-safe DN string encoding
*
* @param string $str DN string
*
* @return string Encoded HTML identifier string
*/
static function dn_encode($str)
{
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
/**
* Decodes DN string encoded with _dn_encode()
*
* @param string $str Encoded HTML identifier string
*
* @return string DN string
*/
static function dn_decode($str)
{
$str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
return base64_decode($str);
}
}
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc
index 1f7a573..01e1ac2 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc
@@ -1,3 +1,10 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Влизане като';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc
index 5e85a01..0332070 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc
@@ -1,3 +1,10 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Anmelden als';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc
index 5e85a01..3918e6e 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc
@@ -1,3 +1,11 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Anmelden als';
+$labels['loginasnotallowed'] = 'Keine Privilegien zum Anmelden als $user';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc
index e1adb3f..4882bdc 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc
@@ -1,5 +1,14 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
+
$labels['loginas'] = 'Login As';
+$labels['loginasnotallowed'] = 'No privileges to login as $user';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc
index acb6c35..ed203e6 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc
@@ -1,2 +1,9 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc
index acb6c35..ed203e6 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc
@@ -1,2 +1,9 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc
index 6f72695..6538f5b 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc
@@ -1,3 +1,11 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Se connecter en tant que';
+$labels['loginasnotallowed'] = 'Pas de privilège de se connecter comme $utilisateur';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc
index ed0358a..e360737 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc
@@ -1,3 +1,10 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'ログイン';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc
index a98283f..ea3a1c0 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc
@@ -1,3 +1,10 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Log in als';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc
index 124c373..ca67859 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc
@@ -1,3 +1,10 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Zaloguj jako';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc
index 26e9541..e594c2e 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc
@@ -1,3 +1,10 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Logar como';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc
index 9e28c12..ac9e5a7 100644
--- a/lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc
@@ -1,3 +1,11 @@
<?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
$labels['loginas'] = 'Войти как';
+$labels['loginasnotallowed'] = 'Нет привилегий войти как $user';
?>
diff --git a/lib/drivers/kolab/plugins/kolab_auth/package.xml b/lib/drivers/kolab/plugins/kolab_auth/package.xml
deleted file mode 100644
index 5a2093b..0000000
--- a/lib/drivers/kolab/plugins/kolab_auth/package.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
- http://pear.php.net/dtd/tasks-1.0.xsd
- http://pear.php.net/dtd/package-2.0
- http://pear.php.net/dtd/package-2.0.xsd">
- <name>kolab_auth</name>
- <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
- <summary>Kolab Authentication</summary>
- <description>
- Authenticates on LDAP server, finds canonized authentication ID for IMAP
- and for new users creates identity based on LDAP information.
- Supports impersonate feature (login as another user). To use this feature
- imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
- </description>
- <lead>
- <name>Aleksander Machniak</name>
- <user>machniak</user>
- <email>machniak@kolabsys.com</email>
- <active>yes</active>
- </lead>
- <date>2013-10-04</date>
- <version>
- <release>1.0</release>
- <api>1.0</api>
- </version>
- <stability>
- <release>stable</release>
- <api>stable</api>
- </stability>
- <license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
- <notes>-</notes>
- <contents>
- <dir baseinstalldir="/" name="/">
- <file name="kolab_auth.php" role="php">
- <tasks:replace from="@name@" to="name" type="package-info"/>
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="kolab_auth_ldap.php" role="php">
- <tasks:replace from="@name@" to="name" type="package-info"/>
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="config.inc.php.dist" role="data"></file>
- <file name="LICENSE" role="data"></file>
-
- <file name="localization/de_CH.inc" role="data"></file>
- <file name="localization/de_DE.inc" role="data"></file>
- <file name="localization/en_US.inc" role="data"></file>
- <file name="localization/pl_PL.inc" role="data"></file>
- </dir>
- <!-- / -->
- </contents>
- <dependencies>
- <required>
- <php>
- <min>5.2.1</min>
- </php>
- <pearinstaller>
- <min>1.7.0</min>
- </pearinstaller>
- </required>
- </dependencies>
- <phprelease/>
-</package>
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
index 2aa046d..98e7e78 100644
--- a/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,187 +1,187 @@
/**
* libkolab database schema
*
* @version 1.1
* @author Thomas Bruederli
* @licence GNU AGPL
**/
DROP TABLE IF EXISTS `kolab_folders`;
CREATE TABLE `kolab_folders` (
`folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`resource` VARCHAR(255) NOT NULL,
`type` VARCHAR(32) NOT NULL,
`synclock` INT(10) NOT NULL DEFAULT '0',
`ctag` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY(`folder_id`),
INDEX `resource_type` (`resource`, `type`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache`;
DROP TABLE IF EXISTS `kolab_cache_contact`;
CREATE TABLE `kolab_cache_contact` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
`name` VARCHAR(255) NOT NULL,
`firstname` VARCHAR(255) NOT NULL,
`surname` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `contact_type` (`folder_id`,`type`),
INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_event`;
CREATE TABLE `kolab_cache_event` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_task`;
CREATE TABLE `kolab_cache_task` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_journal`;
CREATE TABLE `kolab_cache_journal` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_note`;
CREATE TABLE `kolab_cache_note` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_file`;
CREATE TABLE `kolab_cache_file` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`filename` varchar(255) DEFAULT NULL,
CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `folder_filename` (`folder_id`, `filename`),
INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_configuration`;
CREATE TABLE `kolab_cache_configuration` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `configuration_type` (`folder_id`,`type`),
INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_freebusy`;
CREATE TABLE `kolab_cache_freebusy` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
- `tags` VARCHAR(255) NOT NULL,
+ `tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2014021000');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2015011600');
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014112700.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014112700.sql
new file mode 100644
index 0000000..90c77b8
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014112700.sql
@@ -0,0 +1,2 @@
+-- delete cache entries for old folder identifiers
+DELETE FROM `kolab_folders` WHERE `resource` LIKE 'imap://anonymous@%';
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2015011600.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2015011600.sql
new file mode 100644
index 0000000..be523ae
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2015011600.sql
@@ -0,0 +1,8 @@
+ALTER TABLE `kolab_cache_contact` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_event` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_task` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_journal` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_note` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_file` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_configuration` MODIFY `tags` text NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` MODIFY `tags` text NOT NULL;
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql b/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql
index 2c078cb..8f1ed64 100644
--- a/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql
@@ -1,184 +1,184 @@
/**
* libkolab database schema
*
* @version 1.1
* @author Aleksander Machniak
* @licence GNU AGPL
**/
CREATE TABLE "kolab_folders" (
"folder_id" number NOT NULL PRIMARY KEY,
"resource" VARCHAR(255) NOT NULL,
"type" VARCHAR(32) NOT NULL,
"synclock" integer DEFAULT 0 NOT NULL,
"ctag" VARCHAR(40) DEFAULT NULL
);
CREATE INDEX "kolab_folders_resource_idx" ON "kolab_folders" ("resource", "type");
CREATE SEQUENCE "kolab_folders_seq"
START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE TRIGGER "kolab_folders_seq_trig"
BEFORE INSERT ON "kolab_folders" FOR EACH ROW
BEGIN
:NEW."folder_id" := "kolab_folders_seq".nextval;
END;
-
+/
CREATE TABLE "kolab_cache_contact" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"type" varchar(32) NOT NULL,
"name" varchar(255) DEFAULT NULL,
"firstname" varchar(255) DEFAULT NULL,
"surname" varchar(255) DEFAULT NULL,
"email" varchar(255) DEFAULT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_contact_type_idx" ON "kolab_cache_contact" ("folder_id", "type");
CREATE INDEX "kolab_cache_contact_uid2msguid" ON "kolab_cache_contact" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_event" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"dtstart" timestamp DEFAULT NULL,
"dtend" timestamp DEFAULT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_event_uid2msguid" ON "kolab_cache_event" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_task" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"dtstart" timestamp DEFAULT NULL,
"dtend" timestamp DEFAULT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_task_uid2msguid" ON "kolab_cache_task" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_journal" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"dtstart" timestamp DEFAULT NULL,
"dtend" timestamp DEFAULT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_journal_uid2msguid" ON "kolab_cache_journal" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_note" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_note_uid2msguid" ON "kolab_cache_note" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_file" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"filename" varchar(255) DEFAULT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_file_filename" ON "kolab_cache_file" ("folder_id", "filename");
CREATE INDEX "kolab_cache_file_uid2msguid" ON "kolab_cache_file" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_configuration" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"type" varchar(32) NOT NULL,
PRIMARY KEY ("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_config_type" ON "kolab_cache_configuration" ("folder_id", "type");
CREATE INDEX "kolab_cache_config_uid2msguid" ON "kolab_cache_configuration" ("folder_id", "uid", "msguid");
CREATE TABLE "kolab_cache_freebusy" (
"folder_id" number NOT NULL
REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
"msguid" number NOT NULL,
"uid" varchar(128) NOT NULL,
"created" timestamp DEFAULT NULL,
"changed" timestamp DEFAULT NULL,
"data" clob NOT NULL,
"xml" clob NOT NULL,
- "tags" varchar(255) DEFAULT NULL,
+ "tags" clob DEFAULT NULL,
"words" clob DEFAULT NULL,
"dtstart" timestamp DEFAULT NULL,
"dtend" timestamp DEFAULT NULL,
PRIMARY KEY("folder_id", "msguid")
);
CREATE INDEX "kolab_cache_fb_uid2msguid" ON "kolab_cache_freebusy" ("folder_id", "uid", "msguid");
-INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2014021000');
+INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2015011600');
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/oracle/2015011600.sql b/lib/drivers/kolab/plugins/libkolab/SQL/oracle/2015011600.sql
new file mode 100644
index 0000000..69f7953
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/oracle/2015011600.sql
@@ -0,0 +1,40 @@
+-- direct change from varchar to clob does not work, need temp column (#4257)
+ALTER TABLE "kolab_cache_contact" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_contact" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_contact" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_contact" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_event" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_event" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_event" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_event" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_task" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_task" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_task" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_task" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_journal" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_journal" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_journal" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_journal" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_note" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_note" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_note" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_note" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_file" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_file" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_file" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_file" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_configuration" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_configuration" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_configuration" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_configuration" RENAME COLUMN "tags1" TO "tags";
+
+ALTER TABLE "kolab_cache_freebusy" ADD "tags1" clob DEFAULT NULL;
+UPDATE "kolab_cache_freebusy" SET "tags1" = "tags";
+ALTER TABLE "kolab_cache_freebusy" DROP COLUMN "tags";
+ALTER TABLE "kolab_cache_freebusy" RENAME COLUMN "tags1" TO "tags";
diff --git a/lib/drivers/kolab/plugins/libkolab/bin/readcache.sh b/lib/drivers/kolab/plugins/libkolab/bin/readcache.sh
new file mode 100755
index 0000000..7e6a3a3
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/bin/readcache.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * Kolab storage cache testing script
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALL_PATH', realpath('.') . '/' );
+ini_set('display_errors', 1);
+libxml_use_internal_errors(true);
+
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
+ die("Execute this from the Roundcube installation dir!\n\n");
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+function print_usage()
+{
+ print "Usage: readcache.sh [OPTIONS] FOLDER\n";
+ print "-h, --host IMAP host name\n";
+ print "-l, --limit Limit the number of records to be listed\n";
+}
+
+// read arguments
+$opts = get_opt(array(
+ 'h' => 'host',
+ 'l' => 'limit',
+ 'v' => 'verbose',
+));
+
+$folder = $opts[0];
+$imap_host = $opts['host'];
+
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+
+if (empty($imap_host)) {
+ $default_host = $rcmail->config->get('default_host');
+ if (is_array($default_host)) {
+ list($k,$v) = each($default_host);
+ $imap_host = is_numeric($k) ? $v : $k;
+ }
+ else {
+ $imap_host = $default_host;
+ }
+
+ // strip protocol prefix
+ $imap_host = preg_replace('!^[a-z]+://!', '', $imap_host);
+}
+
+if (empty($folder) || empty($imap_host)) {
+ print_usage();
+ exit;
+}
+
+// connect to database
+$db = $rcmail->get_dbh();
+$db->db_connect('r');
+if (!$db->is_connected() || $db->is_error())
+ die("No DB connection\n");
+
+
+// resolve folder_id
+if (!is_numeric($folder)) {
+ if (strpos($folder, '@')) {
+ list($mailbox, $domain) = explode('@', $folder);
+ list($username, $subpath) = explode('/', preg_replace('!^user/!', '', $mailbox), 2);
+ $folder_uri = 'imap://' . urlencode($username.'@'.$domain) . '@' . $imap_host . '/' . $subpath;
+ }
+ else {
+ die("Invalid mailbox identifier! Example: user/john.doe/Calendar@example.org\n");
+ }
+
+ print "Resolving folder $folder_uri...";
+ $sql_result = $db->query('SELECT * FROM `kolab_folders` WHERE `resource`=?', $folder_uri);
+ if ($sql_result && ($folder_data = $db->fetch_assoc($sql_result))) {
+ $folder_id = $folder_data['folder_id'];
+ print $folder_id;
+ }
+ print "\n";
+}
+else {
+ $folder_id = intval($folder);
+ $sql_result = $db->query('SELECT * FROM `kolab_folders` WHERE `folder_id`=?', $folder_id);
+ if ($sql_result) {
+ $folder_data = $db->fetch_assoc($sql_result);
+ }
+}
+
+if (empty($folder_data)) {
+ die("Can't find cache mailbox for '$folder'\n");
+}
+
+print "Querying cache for folder $folder_id ($folder_data[type])...\n";
+
+$extra_cols = array(
+ 'event' => array('dtstart','dtend'),
+ 'contact' => array('type'),
+);
+
+$cache_table = $db->table_name('kolab_cache_' . $folder_data['type']);
+$extra_cols_ = $extra_cols[$folder_data['type']] ?: array();
+$sql_arr = $db->fetch_assoc($db->query("SELECT COUNT(*) as cnt FROM `$cache_table` WHERE `folder_id`=?", intval($folder_id)));
+
+print "CTag = " . $folder_data['ctag'] . "\n";
+print "Lock = " . $folder_data['synclock'] . "\n";
+print "Count = " . $sql_arr['cnt'] . "\n";
+print "----------------------------------------------------------------------------------\n";
+print "<MSG>\t<UUID>\t<CHANGED>\t<DATA>\t<XML>\t";
+print join("\t", array_map(function($c) { return '<' . strtoupper($c) . '>'; }, $extra_cols_));
+print "\n----------------------------------------------------------------------------------\n";
+
+$result = $db->limitquery("SELECT * FROM `$cache_table` WHERE `folder_id`=?", 0, $opts['limit'], intval($folder_id));
+while ($result && ($sql_arr = $db->fetch_assoc($result))) {
+ print $sql_arr['msguid'] . "\t" . $sql_arr['uid'] . "\t" . $sql_arr['changed'];
+
+ // try to unserialize data block
+ $object = @unserialize(@base64_decode($sql_arr['data']));
+ print "\t" . ($object === false ? 'FAIL!' : ($object['uid'] == $sql_arr['uid'] ? 'OK' : '!!!'));
+
+ // check XML validity
+ $xml = simplexml_load_string($sql_arr['xml']);
+ print "\t" . ($xml === false ? 'FAIL!' : 'OK');
+
+ // print extra cols
+ array_walk($extra_cols_, function($c) use ($sql_arr) {
+ print "\t" . $sql_arr[$c];
+ });
+
+ print "\n";
+}
+
+print "----------------------------------------------------------------------------------\n";
+echo "Done.\n";
diff --git a/lib/drivers/kolab/plugins/libkolab/composer.json b/lib/drivers/kolab/plugins/libkolab/composer.json
index 8926037..b458df6 100644
--- a/lib/drivers/kolab/plugins/libkolab/composer.json
+++ b/lib/drivers/kolab/plugins/libkolab/composer.json
@@ -1,30 +1,30 @@
{
"name": "kolab/libkolab",
"type": "roundcube-plugin",
"description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
"homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
"license": "AGPLv3",
- "version": "1.1.0",
+ "version": "3.2.3",
"authors": [
{
"name": "Thomas Bruederli",
"email": "bruederli@kolabsys.com",
"role": "Lead"
},
{
"name": "Alensader Machniak",
"email": "machniak@kolabsys.com",
"role": "Developer"
}
],
"repositories": [
{
"type": "composer",
"url": "http://plugins.roundcube.net"
}
],
"require": {
"php": ">=5.3.0",
"roundcube/plugin-installer": ">=0.1.3"
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist b/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
index 79d2aa8..7efa8d1 100644
--- a/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
+++ b/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
@@ -1,61 +1,62 @@
<?php
/* Configuration for libkolab */
// Enable caching of Kolab objects in local database
$config['kolab_cache'] = true;
// Specify format version to write Kolab objects (must be a string value!)
$config['kolab_format_version'] = '3.0';
// Optional override of the URL to read and trigger Free/Busy information of Kolab users
// Defaults to https://<imap-server->/freebusy
$config['kolab_freebusy_server'] = null;
// Enables listing of only subscribed folders. This e.g. will limit
// folders in calendar view or available addressbooks
$config['kolab_use_subscriptions'] = false;
// List any of 'personal','shared','other' namespaces to be excluded from groupware folder listing
// example: array('other');
$config['kolab_skip_namespace'] = null;
// Enables the use of displayname folder annotations as introduced in KEP:?
// for displaying resource folder names (experimental!)
$config['kolab_custom_display_names'] = false;
// Configuration of HTTP requests.
// See http://pear.php.net/manual/en/package.http.http-request2.config.php
// for list of supported configuration options (array keys)
$config['kolab_http_request'] = array();
// When kolab_cache is enabled Roundcube's messages cache will be redundant
// when working on kolab folders. Here we can:
// 2 - bypass messages/indexes cache completely
// 1 - bypass only messages, but use index cache
$config['kolab_messages_cache_bypass'] = 0;
// LDAP directory to find avilable users for folder sharing.
// Either contains an array with LDAP addressbook configuration or refers to entry in $config['ldap_public'].
// If not specified, the configuraton from 'kolab_auth_addressbook' will be used.
+// Should be provided for multi-domain setups with placeholders like %dc, %d, %u, %fu or %dn.
$config['kolab_users_directory'] = null;
// Filter to be used for resolving user folders in LDAP.
// Defaults to the 'kolab_auth_filter' configuration option.
$config['kolab_users_filter'] = '(&(objectclass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)))';
// Which property of the LDAP user record to use for user folder mapping in IMAP.
// Defaults to the 'kolab_auth_login' configuration option.
$config['kolab_users_id_attrib'] = null;
// Use these attributes when searching users in LDAP
$config['kolab_users_search_attrib'] = array('cn','mail','alias');
// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects
$config['kolab_bonnie_api'] = array(
'uri' => 'https://<kolab-hostname>:8080/api/rpc',
'user' => 'webclient',
'pass' => 'Welcome2KolabSystems',
'secret' => '8431f191707fffffff00000000cccc',
'debug' => true, // logs requests/responses to <log-dir>/bonnie
);
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php
index 23dafd8..e8ac131 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php
@@ -1,82 +1,82 @@
<?php
/**
* Provider class for accessing historic groupware object data through the Bonnie service
*
* API Specification at https://wiki.kolabsys.com/User:Bruederli/Draft:Bonnie_Client_API
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_bonnie_api
{
public $ready = false;
private $config = array();
private $client = null;
/**
* Default constructor
*/
public function __construct($config)
{
- $this->config = $confg;
+ $this->config = $config;
$this->client = new kolab_bonnie_api_client($config['uri'], $config['timeout'] ?: 5, (bool)$config['debug']);
$this->client->set_secret($config['secret']);
$this->client->set_authentication($config['user'], $config['pass']);
$this->client->set_request_user(rcube::get_instance()->get_user_name());
$this->ready = !empty($config['secret']) && !empty($config['user']) && !empty($config['pass']);
}
/**
* Wrapper function for <object>.changelog() API call
*/
public function changelog($type, $uid, $mailbox=null)
{
return $this->client->execute($type.'.changelog', array('uid' => $uid, 'mailbox' => $mailbox));
}
/**
* Wrapper function for <object>.diff() API call
*/
public function diff($type, $uid, $rev, $mailbox=null)
{
return $this->client->execute($type.'.diff', array('uid' => $uid, 'rev' => $rev, 'mailbox' => $mailbox));
}
/**
* Wrapper function for <object>.get() API call
*/
public function get($type, $uid, $rev, $mailbox=null)
{
return $this->client->execute($type.'.get', array('uid' => $uid, 'rev' => intval($rev), 'mailbox' => $mailbox));
}
/**
* Generic wrapper for direct API calls
*/
public function _execute($method, $params = array())
{
return $this->client->execute($method, $params);
}
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
index 8c6b1d4..625483b 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
@@ -1,699 +1,699 @@
<?php
/**
* Kolab format model class wrapping libkolabxml bindings
*
* Abstract base class for different Kolab groupware objects read from/written
* to the new Kolab 3 format using the PHP bindings of libkolabxml.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_format
{
public static $timezone;
public /*abstract*/ $CTYPE;
public /*abstract*/ $CTYPEv2;
protected /*abstract*/ $objclass;
protected /*abstract*/ $read_func;
protected /*abstract*/ $write_func;
protected $obj;
protected $data;
protected $xmldata;
protected $xmlobject;
protected $formaterror;
protected $loaded = false;
protected $version = '3.0';
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
const PRODUCT_ID = 'Roundcube-libkolab-1.1';
// mapping table for valid PHP timezones not supported by libkolabxml
// basically the entire list of ftp://ftp.iana.org/tz/data/backward
protected static $timezone_map = array(
'Africa/Asmera' => 'Africa/Asmara',
'Africa/Timbuktu' => 'Africa/Abidjan',
'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
'America/Atka' => 'America/Adak',
'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
'America/Catamarca' => 'America/Argentina/Catamarca',
'America/Coral_Harbour' => 'America/Atikokan',
'America/Cordoba' => 'America/Argentina/Cordoba',
'America/Ensenada' => 'America/Tijuana',
'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
'America/Indianapolis' => 'America/Indiana/Indianapolis',
'America/Jujuy' => 'America/Argentina/Jujuy',
'America/Knox_IN' => 'America/Indiana/Knox',
'America/Louisville' => 'America/Kentucky/Louisville',
'America/Mendoza' => 'America/Argentina/Mendoza',
'America/Porto_Acre' => 'America/Rio_Branco',
'America/Rosario' => 'America/Argentina/Cordoba',
'America/Virgin' => 'America/Port_of_Spain',
'Asia/Ashkhabad' => 'Asia/Ashgabat',
'Asia/Calcutta' => 'Asia/Kolkata',
'Asia/Chungking' => 'Asia/Shanghai',
'Asia/Dacca' => 'Asia/Dhaka',
'Asia/Katmandu' => 'Asia/Kathmandu',
'Asia/Macao' => 'Asia/Macau',
'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
'Asia/Tel_Aviv' => 'Asia/Jerusalem',
'Asia/Thimbu' => 'Asia/Thimphu',
'Asia/Ujung_Pandang' => 'Asia/Makassar',
'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
'Atlantic/Faeroe' => 'Atlantic/Faroe',
'Atlantic/Jan_Mayen' => 'Europe/Oslo',
'Australia/ACT' => 'Australia/Sydney',
'Australia/Canberra' => 'Australia/Sydney',
'Australia/LHI' => 'Australia/Lord_Howe',
'Australia/NSW' => 'Australia/Sydney',
'Australia/North' => 'Australia/Darwin',
'Australia/Queensland' => 'Australia/Brisbane',
'Australia/South' => 'Australia/Adelaide',
'Australia/Tasmania' => 'Australia/Hobart',
'Australia/Victoria' => 'Australia/Melbourne',
'Australia/West' => 'Australia/Perth',
'Australia/Yancowinna' => 'Australia/Broken_Hill',
'Brazil/Acre' => 'America/Rio_Branco',
'Brazil/DeNoronha' => 'America/Noronha',
'Brazil/East' => 'America/Sao_Paulo',
'Brazil/West' => 'America/Manaus',
'Canada/Atlantic' => 'America/Halifax',
'Canada/Central' => 'America/Winnipeg',
'Canada/East-Saskatchewan' => 'America/Regina',
'Canada/Eastern' => 'America/Toronto',
'Canada/Mountain' => 'America/Edmonton',
'Canada/Newfoundland' => 'America/St_Johns',
'Canada/Pacific' => 'America/Vancouver',
'Canada/Saskatchewan' => 'America/Regina',
'Canada/Yukon' => 'America/Whitehorse',
'Chile/Continental' => 'America/Santiago',
'Chile/EasterIsland' => 'Pacific/Easter',
'Cuba' => 'America/Havana',
'Egypt' => 'Africa/Cairo',
'Eire' => 'Europe/Dublin',
'Europe/Belfast' => 'Europe/London',
'Europe/Tiraspol' => 'Europe/Chisinau',
'GB' => 'Europe/London',
'GB-Eire' => 'Europe/London',
'Greenwich' => 'Etc/GMT',
'Hongkong' => 'Asia/Hong_Kong',
'Iceland' => 'Atlantic/Reykjavik',
'Iran' => 'Asia/Tehran',
'Israel' => 'Asia/Jerusalem',
'Jamaica' => 'America/Jamaica',
'Japan' => 'Asia/Tokyo',
'Kwajalein' => 'Pacific/Kwajalein',
'Libya' => 'Africa/Tripoli',
'Mexico/BajaNorte' => 'America/Tijuana',
'Mexico/BajaSur' => 'America/Mazatlan',
'Mexico/General' => 'America/Mexico_City',
'NZ' => 'Pacific/Auckland',
'NZ-CHAT' => 'Pacific/Chatham',
'Navajo' => 'America/Denver',
'PRC' => 'Asia/Shanghai',
'Pacific/Ponape' => 'Pacific/Pohnpei',
'Pacific/Samoa' => 'Pacific/Pago_Pago',
'Pacific/Truk' => 'Pacific/Chuuk',
'Pacific/Yap' => 'Pacific/Chuuk',
'Poland' => 'Europe/Warsaw',
'Portugal' => 'Europe/Lisbon',
'ROC' => 'Asia/Taipei',
'ROK' => 'Asia/Seoul',
'Singapore' => 'Asia/Singapore',
'Turkey' => 'Europe/Istanbul',
'UCT' => 'Etc/UCT',
'US/Alaska' => 'America/Anchorage',
'US/Aleutian' => 'America/Adak',
'US/Arizona' => 'America/Phoenix',
'US/Central' => 'America/Chicago',
'US/East-Indiana' => 'America/Indiana/Indianapolis',
'US/Eastern' => 'America/New_York',
'US/Hawaii' => 'Pacific/Honolulu',
'US/Indiana-Starke' => 'America/Indiana/Knox',
'US/Michigan' => 'America/Detroit',
'US/Mountain' => 'America/Denver',
'US/Pacific' => 'America/Los_Angeles',
'US/Samoa' => 'Pacific/Pago_Pago',
'Universal' => 'Etc/UTC',
'W-SU' => 'Europe/Moscow',
'Zulu' => 'Etc/UTC',
);
/**
* Factory method to instantiate a kolab_format object of the given type and version
*
* @param string Object type to instantiate
* @param float Format version
* @param string Cached xml data to initialize with
* @return object kolab_format
*/
public static function factory($type, $version = '3.0', $xmldata = null)
{
if (!isset(self::$timezone))
self::$timezone = new DateTimeZone('UTC');
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
$type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
return new $classname($xmldata, $version);
return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
}
/**
* Determine support for the given format version
*
* @param float Format version to check
* @return boolean True if supported, False otherwise
*/
public static function supports($version)
{
if ($version == '2.0')
return class_exists('kolabobject');
// default is version 3
return class_exists('kolabformat');
}
/**
* Convert the given date/time value into a cDateTime object
*
* @param mixed Date/Time value either as unix timestamp, date string or PHP DateTime object
* @param DateTimeZone The timezone the date/time is in. Use global default if Null, local time if False
* @param boolean True of the given date has no time component
* @return object The libkolabxml date/time object
*/
public static function get_datetime($datetime, $tz = null, $dateonly = false)
{
// use timezone information from datetime of global setting
if (!$tz && $tz !== false) {
if ($datetime instanceof DateTime)
$tz = $datetime->getTimezone();
if (!$tz)
$tz = self::$timezone;
}
$result = new cDateTime();
try {
// got a unix timestamp (in UTC)
if (is_numeric($datetime)) {
$datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
if ($tz) $datetime->setTimezone($tz);
}
else if (is_string($datetime) && strlen($datetime)) {
- $datetime = new DateTime($datetime, $tz ?: null);
+ $datetime = $tz ? new DateTime($datetime, $tz) : new DateTime($datetime);
}
}
catch (Exception $e) {}
if ($datetime instanceof DateTime) {
$result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
if (!$dateonly)
$result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z'))) {
$result->setUTC(true);
}
else if ($tz !== false) {
$tzid = $tz->getName();
if (array_key_exists($tzid, self::$timezone_map))
$tzid = self::$timezone_map[$tzid];
$result->setTimezone($tzid);
}
}
return $result;
}
/**
* Convert the given cDateTime into a PHP DateTime object
*
* @param object cDateTime The libkolabxml datetime object
* @return object DateTime PHP datetime instance
*/
public static function php_datetime($cdt)
{
if (!is_object($cdt) || !$cdt->isValid())
return null;
$d = new DateTime;
$d->setTimezone(self::$timezone);
try {
if ($tzs = $cdt->timezone()) {
$tz = new DateTimeZone($tzs);
$d->setTimezone($tz);
}
else if ($cdt->isUTC()) {
$d->setTimezone(new DateTimeZone('UTC'));
}
}
catch (Exception $e) { }
$d->setDate($cdt->year(), $cdt->month(), $cdt->day());
if ($cdt->isDateOnly()) {
$d->_dateonly = true;
$d->setTime(12, 0, 0); // set time to noon to avoid timezone troubles
}
else {
$d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
}
return $d;
}
/**
* Convert a libkolabxml vector to a PHP array
*
* @param object vector Object
* @return array Indexed array containing vector elements
*/
public static function vector2array($vec, $max = PHP_INT_MAX)
{
$arr = array();
for ($i=0; $i < $vec->size() && $i < $max; $i++)
$arr[] = $vec->get($i);
return $arr;
}
/**
* Build a libkolabxml vector (string) from a PHP array
*
* @param array Array with vector elements
* @return object vectors
*/
public static function array2vector($arr)
{
$vec = new vectors;
foreach ((array)$arr as $val) {
if (strlen($val))
$vec->push($val);
}
return $vec;
}
/**
* Parse the X-Kolab-Type header from MIME messages and return the object type in short form
*
* @param string X-Kolab-Type header value
* @return string Kolab object type (contact,event,task,note,etc.)
*/
public static function mime2object_type($x_kolab_type)
{
return preg_replace(
array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
array( 'dictionary', 'distribution-list'),
substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
);
}
/**
* Default constructor of all kolab_format_* objects
*/
public function __construct($xmldata = null, $version = null)
{
$this->obj = new $this->objclass;
$this->xmldata = $xmldata;
if ($version)
$this->version = $version;
// use libkolab module if available
if (class_exists('kolabobject'))
$this->xmlobject = new XMLObject();
}
/**
* Check for format errors after calling kolabformat::write*()
*
* @return boolean True if there were errors, False if OK
*/
protected function format_errors()
{
$ret = $log = false;
switch (kolabformat::error()) {
case kolabformat::NoError:
$ret = false;
break;
case kolabformat::Warning:
$ret = false;
$uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
$log = "Warning @ $uid";
break;
default:
$ret = true;
$log = "Error";
}
if ($log && !isset($this->formaterror)) {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "kolabformat $log: " . kolabformat::errorMessage(),
), true);
$this->formaterror = $ret;
}
return $ret;
}
/**
* Save the last generated UID to the object properties.
* Should be called after kolabformat::writeXXXX();
*/
protected function update_uid()
{
// get generated UID
if (!$this->data['uid']) {
if ($this->xmlobject) {
$this->data['uid'] = $this->xmlobject->getSerializedUID();
}
if (empty($this->data['uid'])) {
$this->data['uid'] = kolabformat::getSerializedUID();
}
$this->obj->setUid($this->data['uid']);
}
}
/**
* Initialize libkolabxml object with cached xml data
*/
protected function init()
{
if (!$this->loaded) {
if ($this->xmldata) {
$this->load($this->xmldata);
$this->xmldata = null;
}
$this->loaded = true;
}
}
/**
* Get constant value for libkolab's version parameter
*
* @param float Version value to convert
* @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available
*/
protected function libversion($v = null)
{
if (class_exists('kolabobject')) {
$version = $v ?: $this->version;
if ($version <= '2.0')
return kolabobject::KolabV2;
else
return kolabobject::KolabV3;
}
return false;
}
/**
* Determine the correct libkolab(xml) wrapper function for the given call
* depending on the available PHP modules
*/
protected function libfunc($func)
{
if (is_array($func) || strpos($func, '::'))
return $func;
else if (class_exists('kolabobject'))
return array($this->xmlobject, $func);
else
return 'kolabformat::' . $func;
}
/**
* Direct getter for object properties
*/
public function __get($var)
{
return $this->data[$var];
}
/**
* Load Kolab object data from the given XML block
*
* @param string XML data
* @return boolean True on success, False on failure
*/
public function load($xml)
{
$this->formaterror = null;
$read_func = $this->libfunc($this->read_func);
if (is_array($read_func))
$r = call_user_func($read_func, $xml, $this->libversion());
else
$r = call_user_func($read_func, $xml, false);
if (is_resource($r))
$this->obj = new $this->objclass($r);
else if (is_a($r, $this->objclass))
$this->obj = $r;
$this->loaded = !$this->format_errors();
}
/**
* Write object data to XML format
*
* @param float Format version to write
* @return string XML data
*/
public function write($version = null)
{
$this->formaterror = null;
$this->init();
$write_func = $this->libfunc($this->write_func);
if (is_array($write_func))
$this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID);
else
$this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID);
if (!$this->format_errors())
$this->update_uid();
else
$this->xmldata = null;
return $this->xmldata;
}
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
$this->init();
if (!empty($object['uid']))
$this->obj->setUid($object['uid']);
// set some automatic values if missing
if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
$cdt = $this->obj->created();
$object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', new DateTimeZone('UTC'));
if (!$cdt || !$cdt->isValid())
$this->obj->setCreated(self::get_datetime($object['created']));
}
$object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
$this->obj->setLastModified(self::get_datetime($object['changed']));
// Save custom properties of the given object
if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
$vcustom = new vectorcs;
foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
$vcustom->push(new CustomProperty($cp[0], $cp[1]));
}
$this->obj->setCustomProperties($vcustom);
}
// load custom properties from XML for caching (#2238) if method exists (#3125)
else if (method_exists($this->obj, 'customProperties')) {
$object['x-custom'] = array();
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
}
/**
* Convert the Kolab object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Kolab object data as hash array
*/
public function to_array($data = array())
{
$this->init();
// read object properties into local data object
$object = array(
'uid' => $this->obj->uid(),
'changed' => self::php_datetime($this->obj->lastModified()),
);
// not all container support the created property
if (method_exists($this->obj, 'created')) {
$object['created'] = self::php_datetime($this->obj->created());
}
// read custom properties
if (method_exists($this->obj, 'customProperties')) {
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
// merge with additional data, e.g. attachments from the message
if ($data) {
foreach ($data as $idx => $value) {
if (is_array($value)) {
$object[$idx] = array_merge((array)$object[$idx], $value);
}
else {
$object[$idx] = $value;
}
}
}
return $object;
}
/**
* Object validation method to be implemented by derived classes
*/
abstract public function is_valid();
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
return array();
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
return array();
}
/**
* Utility function to extract object attachment data
*
* @param array Hash array reference to append attachment data into
*/
public function get_attachments(&$object)
{
$this->init();
// handle attachments
$vattach = $this->obj->attachments();
for ($i=0; $i < $vattach->size(); $i++) {
$attach = $vattach->get($i);
// skip cid: attachments which are mime message parts handled by kolab_storage_folder
if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
$name = $attach->label();
$key = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
$content = $attach->data();
$object['_attachments'][$key] = array(
'id' => 'i:'.$i,
'name' => $name,
'mimetype' => $attach->mimetype(),
'size' => strlen($content),
'content' => $content,
);
}
else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
$object['links'][] = $attach->uri();
}
}
}
/**
* Utility function to set attachment properties to the kolabformat object
*
* @param array Object data as hash array
* @param boolean True to always overwrite attachment information
*/
protected function set_attachments($object, $write = true)
{
// save attachments
$vattach = new vectorattachment;
foreach ((array) $object['_attachments'] as $cid => $attr) {
if (empty($attr))
continue;
$attach = new Attachment;
$attach->setLabel((string)$attr['name']);
$attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
if ($attach->isValid()) {
$vattach->push($attach);
$write = true;
}
else {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
), true);
}
}
foreach ((array) $object['links'] as $link) {
$attach = new Attachment;
$attach->setUri($link, 'unknown');
$vattach->push($attach);
$write = true;
}
if ($write) {
$this->obj->setAttachments($vattach);
}
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
index 4b06302..ceb7ebb 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
@@ -1,282 +1,284 @@
<?php
/**
* Kolab Configuration data model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_configuration extends kolab_format
{
- public $CTYPE = 'application/x-vnd.kolab.configuration';
+ public $CTYPE = 'application/vnd.kolab+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.configuration';
protected $objclass = 'Configuration';
protected $read_func = 'readConfiguration';
protected $write_func = 'writeConfiguration';
private $type_map = array(
'category' => Configuration::TypeCategoryColor,
'dictionary' => Configuration::TypeDictionary,
'file_driver' => Configuration::TypeFileDriver,
'relation' => Configuration::TypeRelation,
'snippet' => Configuration::TypeSnippet,
);
private $driver_settings_fields = array('host', 'port', 'username', 'password');
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
- // set common object properties
- parent::set($object);
-
// read type-specific properties
switch ($object['type']) {
case 'dictionary':
$dict = new Dictionary($object['language']);
$dict->setEntries(self::array2vector($object['e']));
$this->obj = new Configuration($dict);
break;
case 'category':
// TODO: implement this
$categories = new vectorcategorycolor;
$this->obj = new Configuration($categories);
break;
case 'file_driver':
$driver = new FileDriver($object['driver'], $object['title']);
$driver->setEnabled((bool) $object['enabled']);
foreach ($this->driver_settings_fields as $field) {
$value = $object[$field];
if ($value !== null) {
$driver->{'set' . ucfirst($field)}($value);
}
}
$this->obj = new Configuration($driver);
break;
case 'relation':
$relation = new Relation(strval($object['name']), strval($object['category']));
if ($object['color']) {
$relation->setColor($object['color']);
}
if ($object['parent']) {
$relation->setParent($object['parent']);
}
if ($object['iconName']) {
$relation->setIconName($object['iconName']);
}
if ($object['priority'] > 0) {
$relation->setPriority((int) $object['priority']);
}
if (!empty($object['members'])) {
$relation->setMembers(self::array2vector($object['members']));
}
$this->obj = new Configuration($relation);
break;
case 'snippet':
$collection = new SnippetCollection($object['name']);
$snippets = new vectorsnippets;
foreach ((array) $object['snippets'] as $item) {
$snippet = new snippet($item['name'], $item['text']);
$snippet->setTextType(strtolower($item['type']) == 'html' ? Snippet::HTML : Snippet::Plain);
if ($item['shortcut']) {
$snippet->setShortCut($item['shortcut']);
}
$snippets->push($snippet);
}
$collection->setSnippets($snippets);
$this->obj = new Configuration($collection);
break;
default:
return false;
}
// adjust content-type string
- $this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
+ $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
+
+ // reset old object data, otherwise set() will overwrite current data (#4095)
+ $this->xmldata = null;
+ // set common object properties
+ parent::set($object);
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return $this->data || (is_object($this->obj) && $this->obj->isValid());
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data)) {
return $this->data;
}
// read common object props into local data object
$object = parent::to_array($data);
$type_map = array_flip($this->type_map);
$object['type'] = $type_map[$this->obj->type()];
// read type-specific properties
switch ($object['type']) {
case 'dictionary':
$dict = $this->obj->dictionary();
$object['language'] = $dict->language();
$object['e'] = self::vector2array($dict->entries());
break;
case 'category':
// TODO: implement this
break;
case 'file_driver':
$driver = $this->obj->fileDriver();
$object['driver'] = $driver->driver();
$object['title'] = $driver->title();
$object['enabled'] = $driver->enabled();
foreach ($this->driver_settings_fields as $field) {
$object[$field] = $driver->{$field}();
}
break;
case 'relation':
$relation = $this->obj->relation();
$object['name'] = $relation->name();
$object['category'] = $relation->type();
$object['color'] = $relation->color();
$object['parent'] = $relation->parent();
$object['iconName'] = $relation->iconName();
$object['priority'] = $relation->priority();
$object['members'] = self::vector2array($relation->members());
break;
case 'snippet':
$collection = $this->obj->snippets();
$object['name'] = $collection->name();
$object['snippets'] = array();
$snippets = $collection->snippets();
for ($i=0; $i < $snippets->size(); $i++) {
$snippet = $snippets->get($i);
$object['snippets'][] = array(
'name' => $snippet->name(),
'text' => $snippet->text(),
'type' => $snippet->textType() == Snippet::HTML ? 'html' : 'plain',
'shortcut' => $snippet->shortCut(),
);
}
break;
}
// adjust content-type string
if ($object['type']) {
- $this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
+ $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
}
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = array();
switch ($this->data['type']) {
case 'dictionary':
$tags = array($this->data['language']);
break;
case 'relation':
$tags = array('category:' . $this->data['category']);
break;
}
return $tags;
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$words = array();
foreach ((array)$this->data['members'] as $url) {
$member = kolab_storage_config::parse_member_url($url);
if (empty($member)) {
if (strpos($url, 'urn:uuid:') === 0) {
$words[] = substr($url, 9);
}
}
else if (!empty($member['params']['message-id'])) {
$words[] = $member['params']['message-id'];
}
else {
// derive message identifier from URI
$words[] = md5($url);
}
}
return $words;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
index c233f44..8cad89a 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
@@ -1,236 +1,244 @@
<?php
/**
* Kolab Event model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
protected $objclass = 'Event';
protected $read_func = 'readEvent';
protected $write_func = 'writeEvent';
/**
* Default constructor
*/
function __construct($data = null, $version = 3.0)
{
parent::__construct(is_string($data) ? $data : null, $version);
// got an Event object as argument
if (is_object($data) && is_a($data, $this->objclass)) {
$this->obj = $data;
$this->loaded = true;
}
}
/**
* Clones into an instance of libcalendaring's extended EventCal class
*
* @return mixed EventCal object or false on failure
*/
public function to_libcal()
{
static $error_logged = false;
if (class_exists('kolabcalendaring')) {
return new EventCal($this->obj);
}
else if (!$error_logged) {
$error_logged = true;
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabcalendaring module not found"
), true);
}
return false;
}
/**
* Set event properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
// set common xcal properties
parent::set($object);
// do the hard work of setting object values
$this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
$this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
$this->obj->setTransparency($object['free_busy'] == 'free');
$status = kolabformat::StatusUndefined;
if ($object['free_busy'] == 'tentative')
$status = kolabformat::StatusTentative;
if ($object['cancelled'])
$status = kolabformat::StatusCancelled;
else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
$status = $this->status_map[$object['status']];
$this->obj->setStatus($status);
// save recurrence exceptions
if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
$vexceptions = new vectorevent;
- foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
+ foreach((array)$object['recurrence']['EXCEPTIONS'] as $i => $exception) {
$exevent = new kolab_format_event;
- $exevent->set($this->compact_exception($exception, $object)); // only save differing values
+ $exevent->set(($compacted = $this->compact_exception($exception, $object))); // only save differing values
$exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
$vexceptions->push($exevent->obj);
+ // write cleaned-up exception data back to memory/cache
+ $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
}
$this->obj->setExceptions($vexceptions);
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
(is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
}
/**
* Convert the Event object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Event data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common xcal props
$object = parent::to_array($data);
// read object properties
$object += array(
'end' => self::php_datetime($this->obj->end()),
'allday' => $this->obj->start()->isDateOnly(),
'free_busy' => $this->obj->transparency() ? 'free' : 'busy', // TODO: transparency is only boolean
'attendees' => array(),
);
// derive event end from duration (#1916)
if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
$interval = new DateInterval('PT0S');
$interval->d = $duration->weeks() * 7 + $duration->days();
$interval->h = $duration->hours();
$interval->i = $duration->minutes();
$interval->s = $duration->seconds();
$object['end'] = clone $object['start'];
$object['end']->add($interval);
}
// organizer is part of the attendees list in Roundcube
if ($object['organizer']) {
$object['organizer']['role'] = 'ORGANIZER';
array_unshift($object['attendees'], $object['organizer']);
}
// status defines different event properties...
$status = $this->obj->status();
if ($status == kolabformat::StatusTentative)
$object['free_busy'] = 'tentative';
else if ($status == kolabformat::StatusCancelled)
$object['cancelled'] = true;
// this is an exception object
if ($this->obj->recurrenceID()->isValid()) {
$object['thisandfuture'] = $this->obj->thisAndFuture();
}
// read exception event objects
else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
$recurrence_exceptions = array();
for ($i=0; $i < $exceptions->size(); $i++) {
if (($exobj = $exceptions->get($i))) {
$exception = new kolab_format_event($exobj);
if ($exception->is_valid()) {
$recurrence_exceptions[] = $this->expand_exception($exception->to_array(), $object);
}
}
}
$object['recurrence']['EXCEPTIONS'] = $recurrence_exceptions;
}
return $this->data = $object;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = parent::get_tags();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
return $tags;
}
/**
* Remove some attributes from the exception container
*/
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','attendees','sequence');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
unset($exception[$prop]);
}
}
+ foreach ($master as $prop => $value) {
+ if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
+ unset($exception[$prop]);
+ }
+ }
+
return $exception;
}
/**
* Copy attributes not specified by the exception from the master event
*/
private function expand_exception($exception, $master)
{
foreach ($master as $prop => $value) {
if (empty($exception[$prop]) && !empty($value))
$exception[$prop] = $value;
}
return $exception;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
index 08f27d0..ad54505 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
@@ -1,627 +1,630 @@
<?php
/**
* Xcal based Kolab format class wrapping libkolabxml bindings
*
* Base class for xcal-based Kolab groupware objects such as event, todo, journal
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_format_xcal extends kolab_format
{
public $CTYPE = 'application/calendar+xml';
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
public $scheduling_properties = array('start', 'end', 'location');
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
protected $role_map = array(
'REQ-PARTICIPANT' => kolabformat::Required,
'OPT-PARTICIPANT' => kolabformat::Optional,
'NON-PARTICIPANT' => kolabformat::NonParticipant,
'CHAIR' => kolabformat::Chair,
);
protected $cutype_map = array(
'INDIVIDUAL' => kolabformat::CutypeIndividual,
'GROUP' => kolabformat::CutypeGroup,
'ROOM' => kolabformat::CutypeRoom,
'RESOURCE' => kolabformat::CutypeResource,
'UNKNOWN' => kolabformat::CutypeUnknown,
);
protected $rrule_type_map = array(
'MINUTELY' => RecurrenceRule::Minutely,
'HOURLY' => RecurrenceRule::Hourly,
'DAILY' => RecurrenceRule::Daily,
'WEEKLY' => RecurrenceRule::Weekly,
'MONTHLY' => RecurrenceRule::Monthly,
'YEARLY' => RecurrenceRule::Yearly,
);
protected $weekday_map = array(
'MO' => kolabformat::Monday,
'TU' => kolabformat::Tuesday,
'WE' => kolabformat::Wednesday,
'TH' => kolabformat::Thursday,
'FR' => kolabformat::Friday,
'SA' => kolabformat::Saturday,
'SU' => kolabformat::Sunday,
);
protected $alarm_type_map = array(
'DISPLAY' => Alarm::DisplayAlarm,
'EMAIL' => Alarm::EMailAlarm,
'AUDIO' => Alarm::AudioAlarm,
);
protected $status_map = array(
'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
'CANCELLED' => kolabformat::StatusCancelled,
'TENTATIVE' => kolabformat::StatusTentative,
'CONFIRMED' => kolabformat::StatusConfirmed,
'DRAFT' => kolabformat::StatusDraft,
'FINAL' => kolabformat::StatusFinal,
);
protected $part_status_map = array(
'UNKNOWN' => kolabformat::PartNeedsAction,
'NEEDS-ACTION' => kolabformat::PartNeedsAction,
'TENTATIVE' => kolabformat::PartTentative,
'ACCEPTED' => kolabformat::PartAccepted,
'DECLINED' => kolabformat::PartDeclined,
'DELEGATED' => kolabformat::PartDelegated,
);
/**
* Convert common xcard properties into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Object data as hash array
*/
public function to_array($data = array())
{
// read common object props
$object = parent::to_array($data);
$status_map = array_flip($this->status_map);
$sensitivity_map = array_flip($this->sensitivity_map);
$object += array(
'sequence' => intval($this->obj->sequence()),
'title' => $this->obj->summary(),
'location' => $this->obj->location(),
'description' => $this->obj->description(),
'url' => $this->obj->url(),
'status' => $status_map[$this->obj->status()],
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'priority' => $this->obj->priority(),
'categories' => self::vector2array($this->obj->categories()),
'start' => self::php_datetime($this->obj->start()),
);
if (method_exists($this->obj, 'comment')) {
$object['comment'] = $this->obj->comment();
}
// read organizer and attendees
if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
$object['organizer'] = array(
'email' => $organizer->email(),
'name' => $organizer->name(),
);
}
$role_map = array_flip($this->role_map);
$cutype_map = array_flip($this->cutype_map);
$part_status_map = array_flip($this->part_status_map);
$attvec = $this->obj->attendees();
for ($i=0; $i < $attvec->size(); $i++) {
$attendee = $attvec->get($i);
$cr = $attendee->contact();
if ($cr->email() != $object['organizer']['email']) {
$delegators = $delegatees = array();
$vdelegators = $attendee->delegatedFrom();
for ($j=0; $j < $vdelegators->size(); $j++) {
$delegators[] = $vdelegators->get($j)->email();
}
$vdelegatees = $attendee->delegatedTo();
for ($j=0; $j < $vdelegatees->size(); $j++) {
$delegatees[] = $vdelegatees->get($j)->email();
}
$object['attendees'][] = array(
'role' => $role_map[$attendee->role()],
'cutype' => $cutype_map[$attendee->cutype()],
'status' => $part_status_map[$attendee->partStat()],
'rsvp' => $attendee->rsvp(),
'email' => $cr->email(),
'name' => $cr->name(),
'delegated-from' => $delegators,
'delegated-to' => $delegatees,
);
}
}
// read recurrence rule
if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
$rrule_type_map = array_flip($this->rrule_type_map);
$object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
if ($intvl = $rr->interval())
$object['recurrence']['INTERVAL'] = $intvl;
if (($count = $rr->count()) && $count > 0) {
$object['recurrence']['COUNT'] = $count;
}
else if ($until = self::php_datetime($rr->end())) {
$until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
$object['recurrence']['UNTIL'] = $until;
}
if (($byday = $rr->byday()) && $byday->size()) {
$weekday_map = array_flip($this->weekday_map);
$weekdays = array();
for ($i=0; $i < $byday->size(); $i++) {
$daypos = $byday->get($i);
$prefix = $daypos->occurence();
$weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
}
$object['recurrence']['BYDAY'] = join(',', $weekdays);
}
if (($bymday = $rr->bymonthday()) && $bymday->size()) {
$object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
}
if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
$object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
}
if ($exdates = $this->obj->exceptionDates()) {
for ($i=0; $i < $exdates->size(); $i++) {
if ($exdate = self::php_datetime($exdates->get($i)))
$object['recurrence']['EXDATE'][] = $exdate;
}
}
}
if ($rdates = $this->obj->recurrenceDates()) {
for ($i=0; $i < $rdates->size(); $i++) {
if ($rdate = self::php_datetime($rdates->get($i)))
$object['recurrence']['RDATE'][] = $rdate;
}
}
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
$object['valarms'] = array();
for ($i=0; $i < $valarms->size(); $i++) {
$alarm = $valarms->get($i);
$type = $alarm_types[$alarm->type()];
if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') { // only some alarms are supported
$valarm = array(
'action' => $type,
'summary' => $alarm->summary(),
'description' => $alarm->description(),
);
if ($type == 'EMAIL') {
$valarm['attendees'] = array();
$attvec = $alarm->attendees();
for ($j=0; $j < $attvec->size(); $j++) {
$cr = $attvec->get($j);
$valarm['attendees'][] = $cr->email();
}
}
else if ($type == 'AUDIO') {
$attach = $alarm->audioFile();
$valarm['uri'] = $attach->uri();
}
if ($start = self::php_datetime($alarm->start())) {
$object['alarms'] = '@' . $start->format('U');
$valarm['trigger'] = $start;
}
else if ($offset = $alarm->relativeStart()) {
$prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
$value = $time = '';
if ($w = $offset->weeks()) $value .= $w . 'W';
else if ($d = $offset->days()) $value .= $d . 'D';
else if ($h = $offset->hours()) $time .= $h . 'H';
else if ($m = $offset->minutes()) $time .= $m . 'M';
else if ($s = $offset->seconds()) $time .= $s . 'S';
// assume 'at event time'
if (empty($value) && empty($time)) {
$prefix = '';
$time = '0S';
}
$object['alarms'] = $prefix . $value . $time;
$valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
}
// read alarm duration and repeat properties
if (($duration = $alarm->duration()) && $duration->isValid()) {
$value = $time = '';
if ($w = $duration->weeks()) $value .= $w . 'W';
else if ($d = $duration->days()) $value .= $d . 'D';
else if ($h = $duration->hours()) $time .= $h . 'H';
else if ($m = $duration->minutes()) $time .= $m . 'M';
else if ($s = $duration->seconds()) $time .= $s . 'S';
$valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
$valarm['repeat'] = $alarm->numrepeat();
}
$object['alarms'] .= ':' . $type; // legacy property
$object['valarms'][] = array_filter($valarm);
}
}
$this->get_attachments($object);
return $object;
}
/**
* Set common xcal properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
$this->init();
$is_new = !$this->obj->uid();
$old_sequence = $this->obj->sequence();
$reschedule = $is_new;
// set common object properties
parent::set($object);
// set sequence value
if (!isset($object['sequence'])) {
if ($is_new) {
$object['sequence'] = 0;
}
else {
$object['sequence'] = $old_sequence;
$old = $this->data['uid'] ? $this->data : $this->to_array();
// increment sequence when updating properties relevant for scheduling.
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
// TODO: make the list of properties considered 'significant' for scheduling configurable
foreach ($this->scheduling_properties as $prop) {
$a = $old[$prop];
$b = $object[$prop];
if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
$a = $a->format('Y-m-d');
$b = $b->format('Y-m-d');
}
if ($a != $b) {
$object['sequence']++;
break;
}
}
}
}
$this->obj->setSequence(intval($object['sequence']));
if ($object['sequence'] > $old_sequence) {
$reschedule = true;
}
$this->obj->setSummary($object['title']);
$this->obj->setLocation($object['location']);
$this->obj->setDescription($object['description']);
$this->obj->setPriority($object['priority']);
$this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
$this->obj->setUrl(strval($object['url']));
if (method_exists($this->obj, 'setComment')) {
$this->obj->setComment($object['comment']);
}
// process event attendees
$attendees = new vectorattendee;
foreach ((array)$object['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$object['organizer'] = $attendee;
}
else if ($attendee['email'] != $object['organizer']['email']) {
$cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
$cr->setName($attendee['name']);
+ // set attendee RSVP if missing
+ if (!isset($attendee['rsvp'])) {
+ $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true;
+ }
+
$att = new Attendee;
$att->setContact($cr);
$att->setPartStat($this->part_status_map[$attendee['status']]);
$att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
$att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
- $att->setRSVP((bool)$attendee['rsvp'] || $reschedule);
-
- $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] || $reschedule;
+ $att->setRSVP((bool)$attendee['rsvp']);
if (!empty($attendee['delegated-from'])) {
$vdelegators = new vectorcontactref;
foreach ((array)$attendee['delegated-from'] as $delegator) {
$vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
}
$att->setDelegatedFrom($vdelegators);
}
if (!empty($attendee['delegated-to'])) {
$vdelegatees = new vectorcontactref;
foreach ((array)$attendee['delegated-to'] as $delegatee) {
$vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
}
$att->setDelegatedTo($vdelegatees);
}
if ($att->isValid()) {
$attendees->push($att);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event attendee: " . json_encode($attendee),
), true);
}
}
}
$this->obj->setAttendees($attendees);
if ($object['organizer']) {
$organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
$organizer->setName($object['organizer']['name']);
$this->obj->setOrganizer($organizer);
}
// save recurrence rule
$rr = new RecurrenceRule;
$rr->setFrequency(RecurrenceRule::FreqNone);
if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
$rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
if ($object['recurrence']['INTERVAL'])
$rr->setInterval(intval($object['recurrence']['INTERVAL']));
if ($object['recurrence']['BYDAY']) {
$byday = new vectordaypos;
foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
$occurrence = 0;
if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
$occurrence = intval($m[1]);
$day = $m[2];
}
if (isset($this->weekday_map[$day]))
$byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
}
$rr->setByday($byday);
}
if ($object['recurrence']['BYMONTHDAY']) {
$bymday = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
$bymday->push(intval($day));
$rr->setBymonthday($bymday);
}
if ($object['recurrence']['BYMONTH']) {
$bymonth = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
$bymonth->push(intval($month));
$rr->setBymonth($bymonth);
}
if ($object['recurrence']['COUNT'])
$rr->setCount(intval($object['recurrence']['COUNT']));
else if ($object['recurrence']['UNTIL'])
$rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
if ($rr->isValid()) {
// add exception dates (only if recurrence rule is valid)
$exdates = new vectordatetime;
foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
$exdates->push(self::get_datetime($exdate, null, true));
$this->obj->setExceptionDates($exdates);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
), true);
}
}
$this->obj->setRecurrenceRule($rr);
// save recurrence dates (aka RDATE)
if (!empty($object['recurrence']['RDATE'])) {
$rdates = new vectordatetime;
foreach ((array)$object['recurrence']['RDATE'] as $rdate)
$rdates->push(self::get_datetime($rdate, null, true));
$this->obj->setRecurrenceDates($rdates);
}
// save alarm
$valarms = new vectoralarm;
if ($object['valarms']) {
foreach ($object['valarms'] as $valarm) {
if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
continue; // skip unknown alarm types
}
if ($valarm['action'] == 'EMAIL') {
$recipients = new vectorcontactref;
foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
$recipients->push(new ContactReference(ContactReference::EmailReference, $email));
}
$alarm = new Alarm(
strval($valarm['summary'] ?: $object['title']),
strval($valarm['description'] ?: $object['description']),
$recipients
);
}
else if ($valarm['action'] == 'AUDIO') {
$attach = new Attachment;
$attach->setUri($valarm['uri'] ?: 'null', 'unknown');
$alarm = new Alarm($attach);
}
else {
// action == DISPLAY
$alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
}
if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
$alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
}
else {
try {
$prefix = $valarm['trigger'][0];
$period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
$duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
}
catch (Exception $e) {
// skip alarm with invalid trigger values
rcube::raise_error($e, true);
continue;
}
$alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
}
if ($valarm['duration']) {
try {
$d = new DateInterval($valarm['duration']);
$duration = new Duration($d->d, $d->h, $d->i, $d->s);
$alarm->setDuration($duration, intval($valarm['repeat']));
}
catch (Exception $e) {
// ignore
}
}
$valarms->push($alarm);
}
}
// legacy support
else if ($object['alarms']) {
list($offset, $type) = explode(":", $object['alarms']);
if ($type == 'EMAIL' && !empty($object['_owner'])) { // email alarms implicitly go to event owner
$recipients = new vectorcontactref;
$recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
$alarm = new Alarm($object['title'], strval($object['description']), $recipients);
}
else { // default: display alarm
$alarm = new Alarm($object['title']);
}
if (preg_match('/^@(\d+)/', $offset, $d)) {
$alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
}
else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
$days = $hours = $minutes = $seconds = 0;
switch ($d[3]) {
case 'W': $days = 7*intval($d[2]); break;
case 'D': $days = intval($d[2]); break;
case 'H': $hours = intval($d[2]); break;
case 'M': $minutes = intval($d[2]); break;
case 'S': $seconds = intval($d[2]); break;
}
$alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
}
$valarms->push($alarm);
}
$this->obj->setAlarms($valarms);
$this->set_attachments($object);
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = array();
foreach ((array)$this->data[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = array();
if (!empty($this->data['valarms'])) {
$tags[] = 'x-has-alarms';
}
// create tags reflecting participant status
if (is_array($this->data['attendees'])) {
foreach ($this->data['attendees'] as $attendee) {
if (!empty($attendee['email']) && !empty($attendee['status']))
$tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
}
}
return $tags;
}
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
index dfd1887..47c1e4b 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
@@ -1,1567 +1,1571 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
+ const ERROR_IMAP_CONN = 1;
+ const ERROR_CACHE_DB = 2;
+ const ERROR_NO_PERMISSION = 3;
+ const ERROR_INVALID_FOLDER = 4;
+
public static $version = '3.0';
public static $last_error;
public static $encode_ids = false;
private static $ready = false;
private static $with_tempsubs = true;
private static $subscriptions;
private static $typedata = array();
private static $states;
private static $config;
private static $imap;
private static $ldap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// set imap options
self::$imap->set_options(array(
'skip_deleted' => true,
'threading' => false,
));
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "IMAP server doesn't support METADATA or ANNOTATEMORE"
), true);
}
return self::$ready;
}
/**
* Initializes LDAP object to resolve Kolab users
*/
public static function ldap()
{
if (self::$ldap) {
return self::$ldap;
}
self::setup();
$config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
if (!is_array($config)) {
$ldap_config = (array)self::$config->get('ldap_public');
$config = $ldap_config[$config];
}
if (empty($config)) {
return null;
}
// overwrite filter option
if ($filter = self::$config->get('kolab_users_filter')) {
self::$config->set('kolab_auth_filter', $filter);
}
// re-use the LDAP wrapper class from kolab_auth plugin
require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
self::$ldap = new kolab_auth_ldap($config);
return self::$ldap;
}
-
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
- $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
- return new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return null;
}
-
/**
* Getter for a specific storage folder
*
- * @param string IMAP folder to access (UTF7-IMAP)
+ * @param string IMAP folder to access (UTF7-IMAP)
+ * @param string Expected folder type
+ *
* @return object kolab_storage_folder The folder object
*/
- public static function get_folder($folder)
+ public static function get_folder($folder, $type = null)
{
- return self::setup() ? new kolab_storage_folder($folder) : null;
+ return self::setup() ? new kolab_storage_folder($folder, $type) : null;
}
-
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
- foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+ foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
- $folder = new kolab_storage_folder($foldername);
+ $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
- $folder->set_folder($foldername);
+ $folder->set_folder($foldername, $type, $folderdata[$foldername]);
if ($object = $folder->get_object($uid, '*'))
return $object;
}
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array List of Kolab data objects (each represented as hash array)
* @see kolab_storage_format::select()
*/
public static function select($query, $type)
{
self::setup();
$folder = null;
$result = array();
- foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+ foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
- $folder = new kolab_storage_folder($foldername);
+ $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
- $folder->set_folder($foldername);
+ $folder->set_folder($foldername, $type, $folderdata[$foldername]);
foreach ($folder->select($query, '*') as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Returns Free-busy server URL
*/
public static function get_freebusy_server()
{
$url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
$url = self::$config->get('kolab_freebusy_server', $url);
$url = rcube_utils::resolve_url($url);
return unslashify($url);
}
/**
* Compose an URL to query the free/busy status for the given user
*/
public static function get_freebusy_url($email)
{
return self::get_freebusy_server() . '/' . $email . '.ifb';
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
* @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
public static function folder_id($folder, $enc = null)
{
return $enc == true || ($enc === null && self::$encode_ids) ?
self::id_encode($folder) :
asciiwords(strtr($folder, '/.-', '___'));
}
/**
* Encode the given ID to a safe ascii representation
*
* @param string $id Arbitrary identifier string
*
* @return string Ascii representation
*/
public static function id_encode($id)
{
return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
}
/**
* Convert the given identifier back to it's raw value
*
* @param string $id Ascii identifier
* @return string Raw identifier string
*/
public static function id_decode($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Return the (first) path of the requested IMAP namespace
*
* @param string Namespace name (personal, shared, other)
* @return string IMAP root path for that namespace
*/
public static function namespace_root($name)
{
foreach ((array)self::$imap->get_namespace($name) as $paths) {
if (strlen($paths[0]) > 1) {
return $paths[0];
}
}
return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
'name' => $name,
'subscribe' => $subscribed,
)));
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_rename', array(
'oldname' => $oldname, 'newname' => $newname));
$oldfolder = self::get_folder($oldname);
- $active = self::folder_is_active($oldname);
- $success = self::$imap->rename_folder($oldname, $newname);
+ $active = self::folder_is_active($oldname);
+ $success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
- self::set_state($oldnam, false);
+ self::set_state($oldname, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
* @return mixed New folder name or False on failure
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else
$result = true;
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
self::setup();
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
$found = false;
$namespace = self::$imap->get_namespace();
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username
$pos = strpos($folder, $delim);
if ($pos) {
$prefix = '('.substr($folder, 0, $pos).')';
$folder = substr($folder, $pos+1);
}
else {
$prefix = '('.$folder.')';
$folder = '';
}
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
if ($prefix)
$folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true)) {
$metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
return $name;
}
}
return false;
}
/**
* Helper method to generate a truncated folder name to display.
* Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$diff = 1;
// check if prefix folder is in other users namespace
for ($n = count($names)-1; $n >= 0; $n--) {
if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
$diff = 0;
break;
}
}
$name = str_repeat('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . substr($name, $length);
break;
}
// other users namespace and parent folder exists
else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
$length = strlen('(' . $names[$i] . ') ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type (sorted)
$folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len) {
if ($name == $current) {
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = self::object_name($parent);
}
continue;
}
if (strpos($name, $current.$delim) === 0) {
continue;
}
}
// always show the parent of current folder
if ($p_len && $name == $parent) {
}
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = self::object_name($name);
}
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' &raquo; ') === 0) {
$length = strlen($listnames[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
return $folders;
}
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// get folders types for all folders
if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
$folderdata = self::folders_typedata($prefix);
}
else {
// fetch folder types for the effective list of (subscribed) folders when post-filtering
$folderdata = array();
}
if (!is_array($folderdata)) {
return array();
}
// In some conditions we can skip LIST command (?)
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
// lookup folder type
if (!array_key_exists($folder, $folderdata)) {
$folderdata[$folder] = self::folder_type($folder);
}
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders() with optional post-filtering
*/
protected static function _imap_list_folders($root, $mbox)
{
$postfilter = null;
// compose a post-filter expression for the excluded namespaces
if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$excludes = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = self::namespace_root($ns)) {
$excludes[] = $ns_root;
}
}
if (count($excludes)) {
$postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
}
}
// use normal LIST command to return all folders, it's fast enough
$folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
if (!empty($postfilter)) {
$folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
$folders = self::$imap->sort_folder_list($folders);
}
return $folders;
}
/**
* Search for shared or otherwise not listed groupware folders the user has access
*
* @param string Folder type of folders to search for
* @param string Search string
* @param array Namespace(s) to exclude results from
*
* @return array List of matching kolab_storage_folder objects
*/
public static function search_folders($type, $query, $exclude_ns = array())
{
if (!self::setup()) {
return array();
}
$folders = array();
$query = str_replace('*', '', $query);
// find unsubscribed IMAP folders of the given type
foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
// FIXME: only consider the last part of the folder path for searching?
$realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
if (($query == '' || strpos($realname, $query) !== false) &&
!self::folder_is_subscribed($foldername, true) &&
!in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
) {
- $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ $folders[] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
$pad = ' ';
$out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
foreach ($folders as $folder) {
$folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode &raquo;
}
// $folders is a result of get_folders() we can assume folders were already sorted
foreach (array_keys($nsnames) as $ns) {
asort($nsnames[$ns], SORT_LOCALE_STRING);
foreach (array_keys($nsnames[$ns]) as $utf7name) {
$out[] = $folders[$utf7name];
}
}
return $out;
}
/**
* Check the folder tree and add the missing parents as virtual folders
*
* @param array $folders Folders list
* @param object $tree Reference to the root node of the folder tree
*
* @return array Flat folders list
*/
public static function folder_hierarchy($folders, &$tree = null)
{
$_folders = array();
$delim = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delim);
$tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
$refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
$folder->parent = join($delim, $path);
$folder->children = array(); // reset list
// skip top folders or ones with a custom displayname
if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
$tree->children[] = $folder;
}
else {
$parents = array();
$depth = $folder->get_namespace() == 'personal' ? 1 : 2;
while (count($path) >= $depth && ($parent = join($delim, $path))) {
array_pop($path);
$parent_parent = join($delim, $path);
if (!$refs[$parent]) {
if ($folder->type && self::folder_type($parent) == $folder->type) {
- $refs[$parent] = new kolab_storage_folder($parent, $folder->type);
+ $refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type);
$refs[$parent]->parent = $parent_parent;
}
else if ($parent_parent == $other_ns) {
$refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
}
else {
$name = kolab_storage::object_name($parent, $folder->get_namespace());
$refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
}
$parents[] = $refs[$parent];
}
}
if (!empty($parents)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$parent_node = $refs[$parent->parent] ?: $tree;
$parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
$parent_node = $refs[$folder->parent] ?: $tree;
$parent_node->children[] = $folder;
}
$refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
// return cached result
if (is_array(self::$typedata[$prefix])) {
return self::$typedata[$prefix];
}
$type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
// fetch metadata from *some* folders only
if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$folderdata = $blacklist = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
$blacklist[] = $ns_root;
}
}
foreach (array('personal','other','shared') as $ns) {
if (!in_array($ns, (array)$skip_ns)) {
$ns_root = rtrim(self::namespace_root($ns), $delimiter);
// list top-level folders and their childs one by one
// GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
if ($ns_root == '') {
foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
if (!in_array($folder, $blacklist)) {
$folderdata[$folder] = $metadata;
$opts = self::$imap->folder_attributes($folder);
if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
$folderdata += $data;
}
}
}
}
else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
$folderdata += $data;
}
}
}
}
else {
$folderdata = self::$imap->get_metadata($prefix, $type_keys);
}
if (!is_array($folderdata)) {
return false;
}
// keep list in memory
self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
return self::$typedata[$prefix];
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
- list($ctype, $suffix) = explode('.', $types[self::CTYPE_KEY]);
+ list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
// return in-memory cached result
foreach (self::$typedata as $typedata) {
if (array_key_exists($folder, $typedata)) {
return $typedata[$folder];
}
}
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
public static function set_folder_type($folder, $type='mail')
{
self::setup();
list($ctype, $subtype) = explode('.', $type);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) // fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
return in_array($folder, self::$subscriptions) ||
($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
public static function folder_subscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (self::folder_is_subscribed($folder)) {
return true;
}
else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
$_SESSION['kolab_subscribed_folders'][] = $folder;
return true;
}
}
else if (self::$imap->subscribe($folder)) {
- self::$subscriptions === null;
+ self::$subscriptions = null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
unset($_SESSION['kolab_subscribed_folders'][$i]);
}
return true;
}
else if (self::$imap->unsubscribe($folder)) {
- self::$subscriptions === null;
+ self::$subscriptions = null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
// activation implies temporary subscription
self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
// remove from temp subscriptions, really?
self::folder_unsubscribe($folder, true);
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
self::$states = self::$subscriptions;
$folders = implode(self::$states, '**');
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode(self::$states, '**');
$rcube = rcube::get_instance();
return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array $prop Folder properties
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
/**
*
* @param mixed $query Search value (or array of field => value pairs)
* @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
* @param array $required List of fields that shall ot be empty
* @param int $limit Maximum number of records
* @param int $count Returns the number of records found
*
* @return array List or false on error
*/
public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
{
$query = str_replace('*', '', $query);
// requires a working LDAP setup
if (!self::ldap() || strlen($query) == 0) {
return array();
}
// search users using the configured attributes
$results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
// exclude myself
if ($_SESSION['kolab_dn']) {
unset($results[$_SESSION['kolab_dn']]);
}
// resolve to IMAP folder name
$root = self::namespace_root('other');
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
- list($localpart, $domain) = explode('@', $user[$user_attrib]);
+ list($localpart, ) = explode('@', $user[$user_attrib]);
$user['kolabtargetfolder'] = $root . $localpart;
});
return $results;
}
/**
* Returns a list of IMAP folders shared by the given user
*
* @param array User entry from LDAP
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
{
self::setup();
$folders = array();
// use localpart of user attribute as root for folder listing
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = self::namespace_root('other');
$folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
}
return $folders;
}
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
public static function get_user_folders($type, $subscribed)
{
$folders = $folderdata = array();
if (self::setup()) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
$path = explode($delimiter, $foldername);
// compare folder type if a subfolder is listed
if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
continue;
}
// truncate folder path to top-level folders of the 'other' namespace
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
}
}
// for every (subscribed) user folder, list all (unsubscribed) subfolders
foreach ($folders as $userfolder) {
foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
if (!$folders[$foldername]) {
- $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
$userfolder->children[] = $folders[$foldername];
}
}
}
}
return $folders;
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
index bced3b3..227fa4e 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
@@ -1,1062 +1,1135 @@
<?php
/**
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_cache
{
const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+ public $sync_complete = false;
+
protected $db;
protected $imap;
protected $folder;
protected $uid2msg;
protected $objects;
protected $metadata = array();
protected $folder_id;
protected $resource_uri;
protected $enabled = true;
protected $synched = false;
protected $synclock = false;
protected $ready = false;
protected $cache_table;
protected $folders_table;
protected $max_sql_packet;
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
protected $order_by = null;
protected $limit = null;
+ protected $error = 0;
/**
* Factory constructor
*/
public static function factory(kolab_storage_folder $storage_folder)
{
$subclass = 'kolab_storage_cache_' . $storage_folder->type;
if (class_exists($subclass)) {
return new $subclass($storage_folder);
}
else {
rcube::raise_error(array(
'code' => 900,
'type' => 'php',
'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
), true);
return new kolab_storage_cache($storage_folder);
}
}
/**
* Default constructor
*/
public function __construct(kolab_storage_folder $storage_folder = null)
{
$rcmail = rcube::get_instance();
$this->db = $rcmail->get_dbh();
$this->imap = $rcmail->get_storage();
$this->enabled = $rcmail->config->get('kolab_cache', false);
+ $this->folders_table = $this->db->table_name('kolab_folders');
if ($this->enabled) {
// always read folder cache and lock state from DB master
$this->db->set_table_dsn('kolab_folders', 'w');
// remove sync-lock on script termination
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
}
if ($storage_folder)
$this->set_folder($storage_folder);
}
/**
* Direct access to cache by folder_id
* (only for internal use)
*/
public function select_by_id($folder_id)
{
- $folders_table = $this->db->table_name('kolab_folders', true);
- $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM $folders_table WHERE `folder_id` = ?", $folder_id));
+ $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id));
if ($sql_arr) {
$this->metadata = $sql_arr;
$this->folder_id = $sql_arr['folder_id'];
$this->folder = new StdClass;
$this->folder->type = $sql_arr['type'];
$this->resource_uri = $sql_arr['resource'];
$this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
$this->ready = true;
}
}
/**
* Connect cache with a storage folder
*
* @param kolab_storage_folder The storage folder instance to connect with
*/
public function set_folder(kolab_storage_folder $storage_folder)
{
$this->folder = $storage_folder;
- if (empty($this->folder->name)) {
+ if (empty($this->folder->name) || !$this->folder->valid) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
- $this->folders_table = $this->db->table_name('kolab_folders');
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
$this->ready = $this->enabled && !empty($this->folder->type);
$this->folder_id = null;
}
/**
* Returns true if this cache supports query by type
*/
public function has_type_col()
{
return in_array('type', $this->extra_cols);
}
/**
* Getter for the numeric ID used in cache tables
*/
public function get_folder_id()
{
$this->_read_folder_data();
return $this->folder_id;
}
+ /**
+ * Returns code of last error
+ *
+ * @return int Error code
+ */
+ public function get_error()
+ {
+ return $this->error;
+ }
+
/**
* Synchronize local cache data with remote
*/
public function synchronize()
{
// only sync once per request cycle
if ($this->synched)
return;
// increase time limit
- @set_time_limit($this->max_sync_lock_time);
+ @set_time_limit($this->max_sync_lock_time - 60);
+
+ // get effective time limit we have for synchronization (~70% of the execution time)
+ $time_limit = ini_get('max_execution_time') * 0.7;
+ $sync_start = time();
+
+ // assume sync will be completed
+ $this->sync_complete = true;
if (!$this->ready) {
// kolab cache is disabled, synchronize IMAP mailbox cache only
$this->imap->folder_sync($this->folder->name);
}
else {
// read cached folder metadata
$this->_read_folder_data();
// check cache status hash first ($this->metadata is set in _read_folder_data())
if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
// lock synchronization for this folder or wait if locked
$this->_sync_lock();
// disable messages cache if configured to do so
$this->bypass(true);
// synchronize IMAP mailbox cache
$this->imap->folder_sync($this->folder->name);
// compare IMAP index with object cache index
$imap_index = $this->imap->index($this->folder->name, null, null, true, true);
// determine objects to fetch or to invalidate
if (!$imap_index->is_error()) {
$imap_index = $imap_index->get();
// read cache index
$sql_result = $this->db->query(
"SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
$old_index = array();
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$old_index[] = $sql_arr['msguid'];
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
}
// fetch new objects from imap
+ $i = 0;
foreach (array_diff($imap_index, $old_index) as $msguid) {
if ($object = $this->folder->read_object($msguid, '*')) {
$this->_extended_insert($msguid, $object);
+
+ // check time limit and abort sync if running too long
+ if (++$i % 50 == 0 && time() - $sync_start > $time_limit) {
+ $this->sync_complete = false;
+ break;
+ }
}
}
$this->_extended_insert(0, null);
// delete invalid entries from local DB
$del_index = array_diff($old_index, $imap_index);
if (!empty($del_index)) {
$quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
$this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
$this->folder_id
);
}
// update ctag value (will be written to database in _sync_unlock())
- $this->metadata['ctag'] = $this->folder->get_ctag();
+ if ($this->sync_complete) {
+ $this->metadata['ctag'] = $this->folder->get_ctag();
+ }
}
$this->bypass(false);
// remove lock
$this->_sync_unlock();
}
}
+ $this->check_error();
$this->synched = time();
}
/**
* Read a single entry from cache or from IMAP directly
*
* @param string Related IMAP message UID
* @param string Object type to read
* @param string IMAP folder name the entry relates to
* @param array Hash array with object properties or null if not found
*/
public function get($msguid, $type = null, $foldername = null)
{
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
- return kolab_storage::get_folder($foldername)->cache->get($msguid, $type);
+ $success = false;
+ if ($targetfolder = kolab_storage::get_folder($foldername)) {
+ $success = $targetfolder->cache->get($msguid, $type);
+ $this->error = $targetfolder->cache->get_error();
+ }
+ return $success;
}
// load object if not in memory
if (!isset($this->objects[$msguid])) {
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT * FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id,
$msguid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827)
}
}
// fetch from IMAP if not present in cache
if (empty($this->objects[$msguid])) {
if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
$this->objects = array($msguid => $object);
$this->set($msguid, $object);
}
}
}
+ $this->check_error();
return $this->objects[$msguid];
}
/**
* Insert/Update a cache entry
*
* @param string Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param string IMAP folder name the entry relates to
*/
public function set($msguid, $object, $foldername = null)
{
if (!$msguid) {
return;
}
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
- kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
- return;
+ if ($targetfolder = kolab_storage::get_folder($foldername)) {
+ $targetfolder->cache->set($msguid, $object);
+ $this->error = $targetfolder->cache->get_error();
+ }
+ return;
}
// remove old entry
if ($this->ready) {
$this->_read_folder_data();
$this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id, $msguid);
}
if ($object) {
// insert new object data...
$this->save($msguid, $object);
}
else {
// ...or set in-memory cache to false
$this->objects[$msguid] = $object;
}
+
+ $this->check_error();
}
/**
* Insert (or update) a cache entry
*
* @param int Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param int Optional old message UID (for update)
*/
public function save($msguid, $object, $olduid = null)
{
// write to cache
if ($this->ready) {
$this->_read_folder_data();
$sql_data = $this->_serialize($object);
$sql_data['folder_id'] = $this->folder_id;
$sql_data['msguid'] = $msguid;
$sql_data['uid'] = $object['uid'];
$args = array();
$cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
$cols = array_merge($cols, $this->extra_cols);
foreach ($cols as $idx => $col) {
$cols[$idx] = $this->db->quote_identifier($col);
$args[] = $sql_data[$col];
}
if ($olduid) {
foreach ($cols as $idx => $col) {
$cols[$idx] = "$col = ?";
}
$query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
. " WHERE `folder_id` = ? AND `msguid` = ?";
$args[] = $this->folder_id;
$args[] = $olduid;
}
else {
$query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
. ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
}
$result = $this->db->query($query, $args);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
}
// keep a copy in memory for fast access
$this->objects = array($msguid => $object);
$this->uid2msg = array($object['uid'] => $msguid);
+
+ $this->check_error();
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
* @param object kolab_storage_folder Target storage folder instance
*/
public function move($msguid, $uid, $target)
{
if ($this->ready) {
// clear cached uid mapping and force new lookup
unset($target->cache->uid2msg[$uid]);
// resolve new message UID in target folder
if ($new_msguid = $target->cache->uid2msguid($uid)) {
$this->_read_folder_data();
$this->db->query(
"UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ".
"WHERE `folder_id` = ? AND `msguid` = ?",
$target->cache->get_folder_id(),
$new_msguid,
$this->folder_id,
$msguid
);
$result = $this->db->affected_rows();
}
}
if (empty($result)) {
// just clear cache entry
$this->set($msguid, false);
}
unset($this->uid2msg[$uid]);
+ $this->check_error();
}
/**
* Remove all objects from local cache
*/
- public function purge($type = null)
+ public function purge()
{
if (!$this->ready) {
return true;
}
$this->_read_folder_data();
$result = $this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
return $this->db->affected_rows($result);
}
/**
* Update resource URI for existing cache entries
*
* @param string Target IMAP folder to move it to
*/
public function rename($new_folder)
{
if (!$this->ready) {
return;
}
- $target = kolab_storage::get_folder($new_folder);
+ if ($target = kolab_storage::get_folder($new_folder)) {
+ // resolve new message UID in target folder
+ $this->db->query(
+ "UPDATE `{$this->folders_table}` SET `resource` = ? ".
+ "WHERE `resource` = ?",
+ $target->get_resource_uri(),
+ $this->resource_uri
+ );
- // resolve new message UID in target folder
- $this->db->query(
- "UPDATE `{$this->folders_table}` SET `resource` = ? ".
- "WHERE `resource` = ?",
- $target->get_resource_uri(),
- $this->resource_uri
- );
+ $this->check_error();
+ }
+ else {
+ $this->error = kolab_storage::ERROR_IMAP_CONN;
+ }
}
/**
* Select Kolab objects filtered by the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: array('<colname>', '<comparator>', '<value>')
* @param boolean Set true to only return UIDs instead of complete objects
* @return array List of Kolab data objects (each represented as hash array) or UIDs
*/
public function select($query = array(), $uids = false)
{
$result = $uids ? array() : new kolab_storage_dataset($this);
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
// fetch full object data on one query if a small result set is expected
$fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
$sql_query = "SELECT " . ($fetchall ? '*' : '`msguid` AS `_msguid`, `uid`') . " FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ? " . $this->_sql_where($query);
if (!empty($this->order_by)) {
$sql_query .= ' ORDER BY ' . $this->order_by;
}
$sql_result = $this->limit ?
$this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
$this->db->query($sql_query, $this->folder_id);
if ($this->db->is_error($sql_result)) {
if ($uids) {
return null;
}
$result->set_error(true);
return $result;
}
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($uids) {
$this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
$result[] = $sql_arr['uid'];
}
else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
else if (!$fetchall) {
// only add msguid to dataset index
$result[] = $sql_arr;
}
}
}
// use IMAP
else {
$filter = $this->_query2assoc($query);
if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
$index = $this->imap->search_once($this->folder->name, $search);
}
else {
$index = $this->imap->index($this->folder->name, null, null, true, true);
}
if ($index->is_error()) {
+ $this->check_error();
if ($uids) {
return null;
}
$result->set_error(true);
return $result;
}
$index = $index->get();
$result = $uids ? $index : $this->_fetch($index, $filter['type']);
// TODO: post-filter result according to query
}
// We don't want to cache big results in-memory, however
// if we select only one object here, there's a big chance we will need it later
if (!$uids && count($result) == 1) {
if ($msguid = $result[0]['_msguid']) {
$this->uid2msg[$result[0]['uid']] = $msguid;
$this->objects = array($msguid => $result[0]);
}
}
+ $this->check_error();
+
return $result;
}
/**
* Get number of objects mathing the given query
*
* @param array $query Pseudo-SQL query as list of filter parameter triplets
* @return integer The number of objects of the given type
*/
public function count($query = array())
{
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ?" . $this->_sql_where($query),
$this->folder_id
);
if ($this->db->is_error($sql_result)) {
return null;
}
$sql_arr = $this->db->fetch_assoc($sql_result);
$count = intval($sql_arr['numrows']);
}
// use IMAP
else {
$filter = $this->_query2assoc($query);
if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
$index = $this->imap->search_once($this->folder->name, $search);
}
else {
$index = $this->imap->index($this->folder->name, null, null, true, true);
}
if ($index->is_error()) {
+ $this->check_error();
return null;
}
// TODO: post-filter result according to query
$count = $index->count();
}
+ $this->check_error();
return $count;
}
/**
* Define ORDER BY clause for cache queries
*/
public function set_order_by($sortcols)
{
if (!empty($sortcols)) {
$this->order_by = '`' . join('`, `', (array)$sortcols) . '`';
}
else {
$this->order_by = null;
}
}
/**
* Define LIMIT clause for cache queries
*/
public function set_limit($length, $offset = 0)
{
$this->limit = array($length, $offset);
}
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
*/
protected function _sql_where($query)
{
$sql_where = '';
foreach ((array) $query as $param) {
if (is_array($param[0])) {
$subq = array();
foreach ($param[0] as $q) {
$subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
}
if (!empty($subq)) {
$sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
}
continue;
}
else if ($param[1] == '=' && is_array($param[2])) {
$qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
$param[1] = 'IN';
}
else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
$not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
$param[1] = $not . 'LIKE';
$qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
}
else if ($param[0] == 'tags') {
$param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
$qvalue = $this->db->quote('% '.$param[2].' %');
}
else {
$qvalue = $this->db->quote($param[2]);
}
$sql_where .= sprintf(' AND %s %s %s',
$this->db->quote_identifier($param[0]),
$param[1],
$qvalue
);
}
return $sql_where;
}
/**
* Helper method to convert the given pseudo-query triplets into
* an associative filter array with 'equals' values only
*/
protected function _query2assoc($query)
{
// extract object type from query parameter
$filter = array();
foreach ($query as $param) {
if ($param[1] == '=')
$filter[$param[0]] = $param[2];
}
return $filter;
}
/**
* Fetch messages from IMAP
*
* @param array List of message UIDs to fetch
* @param string Requested object type or * for all
* @param string IMAP folder to read from
* @return array List of parsed Kolab objects
*/
protected function _fetch($index, $type = null, $folder = null)
{
$results = new kolab_storage_dataset($this);
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
$this->set($msguid, $object);
}
}
return $results;
}
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*/
protected function _serialize($object)
{
$sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
if ($object['changed']) {
$sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
}
if ($object['_formatobj']) {
$sql_data['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write(3.0));
$sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search
$sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
}
// extract object data
$data = array();
foreach ($object as $key => $val) {
// skip empty properties
if ($val === "" || $val === null) {
continue;
}
// mark binary data to be extracted from xml on unserialize()
if (isset($this->binary_items[$key])) {
$data[$key] = true;
}
else if ($key[0] != '_') {
$data[$key] = $val;
}
else if ($key == '_attachments') {
foreach ($val as $k => $att) {
unset($att['content'], $att['path']);
if ($att['id'])
$data[$key][$k] = $att;
}
}
}
// use base64 encoding (Bug #1912, #2662)
$sql_data['data'] = base64_encode(serialize($data));
return $sql_data;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
protected function _unserialize($sql_arr)
{
// check if data is a base64-encoded string, for backward compat.
if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
$sql_arr['data'] = base64_decode($sql_arr['data']);
}
$object = unserialize($sql_arr['data']);
// de-serialization failed
if ($object === false) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
), true);
return null;
}
// decode binary properties
foreach ($this->binary_items as $key => $regexp) {
if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
$object[$key] = base64_decode($m[1]);
}
}
$object_type = $sql_arr['type'] ?: $this->folder->type;
$format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
// add meta data
$object['_type'] = $object_type;
$object['_msguid'] = $sql_arr['msguid'];
$object['_mailbox'] = $this->folder->name;
$object['_size'] = strlen($sql_arr['xml']);
$object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
return $object;
}
/**
* Write records into cache using extended inserts to reduce the number of queries to be executed
*
* @param int Message UID. Set 0 to commit buffered inserts
* @param array Kolab object to cache
*/
protected function _extended_insert($msguid, $object)
{
static $buffer = '';
$line = '';
if ($object) {
$sql_data = $this->_serialize($object);
// Skip multifolder insert for Oracle, we can't put long data inline
if ($this->db->db_provider == 'oracle') {
$extra_cols = '';
if ($this->extra_cols) {
$extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
$extra_cols = ', ' . join(', ', $extra_cols);
$extra_args = str_repeat(', ?', count($this->extra_cols));
}
$params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
$sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']);
foreach ($this->extra_cols as $col) {
$params[] = $sql_data[$col];
}
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` "
. " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)"
. " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)",
$params
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
return;
}
$values = array(
$this->db->quote($this->folder_id),
$this->db->quote($msguid),
$this->db->quote($object['uid']),
$this->db->now(),
$this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']),
$this->db->quote($sql_data['xml']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
);
foreach ($this->extra_cols as $col) {
$values[] = $this->db->quote($sql_data[$col]);
}
$line = '(' . join(',', $values) . ')';
}
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
$extra_cols = '';
if ($this->extra_cols) {
$extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
$extra_cols = ', ' . join(', ', $extra_cols);
}
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` ".
" (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)".
" VALUES $buffer"
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
$buffer = '';
}
$buffer .= ($buffer ? ',' : '') . $line;
}
/**
* Returns max_allowed_packet from mysql config
*/
protected function max_sql_packet()
{
if (!$this->max_sql_packet) {
// mysql limit or max 4 MB
$value = $this->db->get_variable('max_allowed_packet', 1048500);
$this->max_sql_packet = min($value, 4*1024*1024) - 2000;
}
return $this->max_sql_packet;
}
/**
* Read this folder's ID and cache metadata
*/
protected function _read_folder_data()
{
// already done
if (!empty($this->folder_id) || !$this->ready)
return;
$sql_arr = $this->db->fetch_assoc($this->db->query(
"SELECT `folder_id`, `synclock`, `ctag`"
. " FROM `{$this->folders_table}` WHERE `resource` = ?",
$this->resource_uri
));
if ($sql_arr) {
$this->metadata = $sql_arr;
$this->folder_id = $sql_arr['folder_id'];
}
else {
$this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
. " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
$this->folder_id = $this->db->insert_id('kolab_folders');
$this->metadata = array();
}
}
/**
* Check lock record for this folder and wait if locked or set lock
*/
protected function _sync_lock()
{
if (!$this->ready)
return;
$this->_read_folder_data();
- $sql_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
// abort if database is not set-up
if ($this->db->is_error()) {
+ $this->check_error();
$this->ready = false;
return;
}
- $this->synclock = true;
+ $read_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
+ $write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?";
- // wait if locked (expire locks after 10 minutes)
- while ($this->metadata && intval($this->metadata['synclock']) > 0 && $this->metadata['synclock'] + $this->max_sync_lock_time > time()) {
+ // wait if locked (expire locks after 10 minutes) ...
+ // ... or if setting lock fails (another process meanwhile set it)
+ while (
+ (intval($this->metadata['synclock']) + $this->max_sync_lock_time > time()) ||
+ (($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock']))) &&
+ !($affected = $this->db->affected_rows($res)))
+ ) {
usleep(500000);
- $this->metadata = $this->db->fetch_assoc($this->db->query($sql_query, $this->folder_id));
+ $this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id));
}
- // set lock
- $this->db->query("UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ?", time(), $this->folder_id);
+ $this->synclock = $affected > 0;
}
/**
* Remove lock for this folder
*/
public function _sync_unlock()
{
if (!$this->ready || !$this->synclock)
return;
$this->db->query(
"UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ? WHERE `folder_id` = ?",
$this->metadata['ctag'],
$this->folder_id
);
$this->synclock = false;
}
+ /**
+ * Check IMAP connection error state
+ */
+ protected function check_error()
+ {
+ if (($err_code = $this->imap->get_error_code()) < 0) {
+ $this->error = kolab_storage::ERROR_IMAP_CONN;
+ if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
+ $this->error = kolab_storage::ERROR_NO_PERMISSION;
+ }
+ }
+ else if ($this->db->is_error()) {
+ $this->error = kolab_storage::ERROR_CACHE_DB;
+ }
+ }
+
/**
* Resolve an object UID into an IMAP message UID
*
* @param string Kolab object UID
* @param boolean Include deleted objects
* @return int The resolved IMAP message UID
*/
public function uid2msguid($uid, $deleted = false)
{
// query local database if available
if (!isset($this->uid2msg[$uid]) && $this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT `msguid` FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
$this->folder_id,
$uid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->uid2msg[$uid] = $sql_arr['msguid'];
}
}
if (!isset($this->uid2msg[$uid])) {
// use IMAP SEARCH to get the right message
$index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
$results = $index->get();
$this->uid2msg[$uid] = end($results);
}
return $this->uid2msg[$uid];
}
/**
* Getter for protected member variables
*/
public function __get($name)
{
if ($name == 'folder_id') {
$this->_read_folder_data();
}
return $this->$name;
}
/**
* Bypass Roundcube messages cache.
* Roundcube cache duplicates information already stored in kolab_cache.
*
* @param bool $disable True disables, False enables messages cache
*/
public function bypass($disable = false)
{
// if kolab cache is disabled do nothing
if (!$this->enabled) {
return;
}
static $messages_cache, $cache_bypass;
if ($messages_cache === null) {
$rcmail = rcube::get_instance();
$messages_cache = (bool) $rcmail->config->get('messages_cache');
$cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass');
}
if ($messages_cache) {
// handle recurrent (multilevel) bypass() calls
if ($disable) {
$this->cache_bypassed += 1;
if ($this->cache_bypassed > 1) {
return;
}
}
else {
$this->cache_bypassed -= 1;
if ($this->cache_bypassed > 0) {
return;
}
}
switch ($cache_bypass) {
case 2:
// Disable messages cache completely
$this->imap->set_messages_caching(!$disable);
break;
case 1:
// We'll disable messages cache, but keep index cache.
// Default mode is both (MODE_INDEX | MODE_MESSAGE)
$mode = rcube_imap_cache::MODE_INDEX;
if (!$disable) {
$mode |= rcube_imap_cache::MODE_MESSAGE;
}
$this->imap->set_messages_caching(true, $mode);
}
}
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
index d58e3c0..036b827 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
@@ -1,840 +1,866 @@
<?php
/**
* Kolab storage class providing access to configuration objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_config
{
const FOLDER_TYPE = 'configuration';
/**
* Singleton instace of kolab_storage_config
*
* @var kolab_storage_config
*/
static protected $instance;
private $folders;
private $default;
private $enabled;
/**
* This implements the 'singleton' design pattern
*
* @return kolab_storage_config The one and only instance
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_storage_config();
}
return self::$instance;
}
/**
* Private constructor (finds default configuration folder as a config source)
*/
private function __construct()
{
// get all configuration folders
$this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
foreach ($this->folders as $folder) {
if ($folder->default) {
$this->default = $folder;
break;
}
}
// if no folder is set as default, choose the first one
if (!$this->default) {
$this->default = reset($this->folders);
}
// attempt to create a default folder if it does not exist
if (!$this->default) {
$folder_name = 'Configuration';
$folder_type = self::FOLDER_TYPE . '.default';
if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
$this->default = new kolab_storage_folder($folder_name, $folder_type);
}
}
// check if configuration folder exist
if ($this->default && $this->default->name) {
$this->enabled = true;
}
}
/**
* Check wether any configuration storage (folder) exists
*
* @return bool
*/
public function is_enabled()
{
return $this->enabled;
}
/**
* Get configuration objects
*
* @param array $filter Search filter
* @param bool $default Enable to get objects only from default folder
* @param int $limit Max. number of records (per-folder)
*
* @return array List of objects
*/
public function get_objects($filter = array(), $default = false, $limit = 0)
{
$list = array();
foreach ($this->folders as $folder) {
// we only want to read from default folder
if ($default && !$folder->default) {
continue;
}
// for better performance it's good to assume max. number of records
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($filter) as $object) {
unset($object['_formatobj']);
$list[] = $object;
}
}
return $list;
}
/**
* Get configuration object
*
* @param string $uid Object UID
* @param bool $default Enable to get objects only from default folder
*
* @return array Object data
*/
public function get_object($uid, $default = false)
{
foreach ($this->folders as $folder) {
// we only want to read from default folder
if ($default && !$folder->default) {
continue;
}
if ($object = $folder->get_object($uid)) {
return $object;
}
}
}
/**
* Create/update configuration object
*
* @param array $object Object data
* @param string $type Object type
*
* @return bool True on success, False on failure
*/
public function save(&$object, $type)
{
if (!$this->enabled) {
return false;
}
$folder = $this->find_folder($object);
if ($type) {
$object['type'] = $type;
}
return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
}
/**
* Remove configuration object
*
* @param string $uid Object UID
*
* @return bool True on success, False on failure
*/
public function delete($uid)
{
if (!$this->enabled) {
return false;
}
// fetch the object to find folder
$object = $this->get_object($uid);
if (!$object) {
return false;
}
$folder = $this->find_folder($object);
return $folder->delete($uid);
}
/**
* Find folder
*/
public function find_folder($object = array())
{
// find folder object
if ($object['_mailbox']) {
foreach ($this->folders as $folder) {
if ($folder->name == $object['_mailbox']) {
break;
}
}
}
else {
$folder = $this->default;
}
return $folder;
}
/**
* Builds relation member URI
*
* @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
*
* @return string $url Member URI
*/
public static function build_member_url($params)
{
// param is object UUID
if (is_string($params) && !empty($params)) {
return 'urn:uuid:' . $params;
}
if (empty($params) || !strlen($params['folder'])) {
return null;
}
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
// modify folder spec. according to namespace
$folder = $params['folder'];
$ns = $storage->folder_namespace($folder);
if ($ns == 'shared') {
// Note: this assumes there's only one shared namespace root
if ($ns = $storage->get_namespace('shared')) {
if ($prefix = $ns[0][0]) {
$folder = 'shared' . substr($folder, strlen($prefix));
}
}
}
else {
if ($ns == 'other') {
// Note: this assumes there's only one other users namespace root
if ($ns = $storage->get_namespace('shared')) {
if ($prefix = $ns[0][0]) {
$folder = 'user' . substr($folder, strlen($prefix));
}
}
}
else {
$folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
}
}
$folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
// build URI
$url = 'imap:///' . $folder;
// UID is optional here because sometimes we want
// to build just a member uri prefix
if ($params['uid']) {
$url .= '/' . $params['uid'];
}
unset($params['folder']);
unset($params['uid']);
if (!empty($params)) {
$url .= '?' . http_build_query($params, '', '&');
}
return $url;
}
/**
* Parses relation member string
*
* @param string $url Member URI
*
* @return array Message folder, UID, Search headers (Message-Id, Date)
*/
public static function parse_member_url($url)
{
// Look for IMAP URI:
// imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
if (strpos($url, 'imap:///') === 0) {
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
// parse_url does not work with imap:/// prefix
$url = parse_url(substr($url, 8));
$path = explode('/', $url['path']);
parse_str($url['query'], $params);
$uid = array_pop($path);
$ns = array_shift($path);
$path = array_map('rawurldecode', $path);
// resolve folder name
if ($ns == 'shared') {
$folder = implode('/', $path);
// Note: this assumes there's only one shared namespace root
if ($ns = $storage->get_namespace('shared')) {
if ($prefix = $ns[0][0]) {
$folder = $prefix . '/' . $folder;
}
}
}
else if ($ns == 'user') {
$username = array_shift($path);
$folder = implode('/', $path);
if ($username != $rcube->get_user_name()) {
// Note: this assumes there's only one other users namespace root
if ($ns = $storage->get_namespace('other')) {
if ($prefix = $ns[0][0]) {
$folder = $prefix . '/' . $username . '/' . $folder;
}
}
}
else if (!strlen($folder)) {
$folder = 'INBOX';
}
}
else {
return;
}
return array(
'folder' => $folder,
'uid' => $uid,
'params' => $params,
);
}
return false;
}
/**
* Build array of member URIs from set of messages
*
* @param string $folder Folder name
* @param array $messages Array of rcube_message objects
*
* @return array List of members (IMAP URIs)
*/
public static function build_members($folder, $messages)
{
$members = array();
foreach ((array) $messages as $msg) {
$params = array(
'folder' => $folder,
'uid' => $msg->uid,
);
// add search parameters:
// we don't want to build "invalid" searches e.g. that
// will return false positives (more or wrong messages)
if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
$params['message-id'] = $messageid;
$params['date'] = $date;
if ($subject = $msg->get('subject', false)) {
$params['subject'] = substr($subject, 0, 256);
}
}
$members[] = self::build_member_url($params);
}
return $members;
}
/**
* Resolve/validate/update members (which are IMAP URIs) of relation object.
*
* @param array $tag Tag object
* @param bool $force Force members list update
*
* @return array Folder/UIDs list
*/
public static function resolve_members(&$tag, $force = true)
{
$result = array();
foreach ((array) $tag['members'] as $member) {
// IMAP URI members
if ($url = self::parse_member_url($member)) {
$folder = $url['folder'];
if (!$force) {
$result[$folder][] = $url['uid'];
}
else {
$result[$folder]['uid'][] = $url['uid'];
$result[$folder]['params'][] = $url['params'];
$result[$folder]['member'][] = $member;
}
}
}
if (empty($result) || !$force) {
return $result;
}
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$search = array();
$missing = array();
// first we search messages by Folder+UID
foreach ($result as $folder => $data) {
// @FIXME: maybe better use index() which is cached?
// @TODO: consider skip_deleted option
$index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
$uids = $index->get();
// messages that were not found need to be searched by search parameters
$not_found = array_diff($data['uid'], $uids);
if (!empty($not_found)) {
foreach ($not_found as $uid) {
$idx = array_search($uid, $data['uid']);
if ($p = $data['params'][$idx]) {
$search[] = $p;
}
$missing[] = $result[$folder]['member'][$idx];
unset($result[$folder]['uid'][$idx]);
unset($result[$folder]['params'][$idx]);
unset($result[$folder]['member'][$idx]);
}
}
$result[$folder] = $uids;
}
// search in all subscribed mail folders using search parameters
if (!empty($search)) {
// remove not found members from the members list
$tag['members'] = array_diff($tag['members'], $missing);
// get subscribed folders
$folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
// @TODO: do this search in chunks (for e.g. 10 messages)?
$search_str = '';
foreach ($search as $p) {
$search_params = array();
foreach ($p as $key => $val) {
$key = strtoupper($key);
// don't search by subject, we don't want false-positives
if ($key != 'SUBJECT') {
$search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
}
}
$search_str .= ' (' . implode(' ', $search_params) . ')';
}
$search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
// search
$search = $storage->search_once($folders, $search_str);
// handle search result
$folders = (array) $search->get_parameters('MAILBOX');
foreach ($folders as $folder) {
$set = $search->get_set($folder);
$uids = $set->get();
if (!empty($uids)) {
$msgs = $storage->fetch_headers($folder, $uids, false);
$members = self::build_members($folder, $msgs);
// merge new members into the tag members list
$tag['members'] = array_merge($tag['members'], $members);
// add UIDs into the result
$result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
}
}
// update tag object with new members list
$tag['members'] = array_unique($tag['members']);
kolab_storage_config::get_instance()->save($tag, 'relation', false);
}
return $result;
}
/**
* Assign tags to kolab objects
*
* @param array $records List of kolab objects
*
* @return array List of tags
*/
public function apply_tags(&$records)
{
// first convert categories into tags
foreach ($records as $i => $rec) {
if (!empty($rec['categories'])) {
$folder = new kolab_storage_folder($rec['_mailbox']);
if ($object = $folder->get_object($rec['uid'])) {
$tags = $rec['categories'];
unset($object['categories']);
unset($records[$i]['categories']);
$this->save_tags($rec['uid'], $tags);
$folder->save($object, $rec['_type'], $rec['uid']);
}
}
}
$tags = array();
// assign tags to objects
foreach ($this->get_tags() as $tag) {
foreach ($records as $idx => $rec) {
$uid = self::build_member_url($rec['uid']);
if (in_array($uid, (array) $tag['members'])) {
$records[$idx]['tags'][] = $tag['name'];
}
}
$tags[] = $tag['name'];
}
$tags = array_unique($tags);
return $tags;
}
/**
* Update object tags
*
* @param string $uid Kolab object UID
* @param array $tags List of tag names
*/
public function save_tags($uid, $tags)
{
$url = self::build_member_url($uid);
$relations = $this->get_tags();
foreach ($relations as $idx => $relation) {
$selected = !empty($tags) && in_array($relation['name'], $tags);
$found = !empty($relation['members']) && in_array($url, $relation['members']);
$update = false;
// remove member from the relation
if ($found && !$selected) {
$relation['members'] = array_diff($relation['members'], (array) $url);
$update = true;
}
// add member to the relation
else if (!$found && $selected) {
$relation['members'][] = $url;
$update = true;
}
if ($update) {
if ($this->save($relation, 'relation')) {
$this->tags[$idx] = $relation; // update in-memory cache
}
}
if ($selected) {
$tags = array_diff($tags, (array)$relation['name']);
}
}
// create new relations
if (!empty($tags)) {
foreach ($tags as $tag) {
$relation = array(
'name' => $tag,
'members' => (array) $url,
'category' => 'tag',
);
if ($this->save($relation, 'relation')) {
$this->tags[] = $relation; // update in-memory cache
}
}
}
}
/**
* Get tags (all or referring to specified object)
*
* @param string $uid Optional object UID
*
* @return array List of Relation objects
*/
public function get_tags($uid = '*')
{
if (!isset($this->tags)) {
$default = true;
$filter = array(
array('type', '=', 'relation'),
array('category', '=', 'tag')
);
// use faster method
if ($uid && $uid != '*') {
$filter[] = array('member', '=', $uid);
$tags = $this->get_objects($filter, $default);
}
else {
$this->tags = $tags = $this->get_objects($filter, $default);
}
}
else {
$tags = $this->tags;
}
if ($uid === '*') {
return $tags;
}
$result = array();
$search = self::build_member_url($uid);
foreach ($tags as $tag) {
if (in_array($search, (array) $tag['members'])) {
$result[] = $tag;
}
}
return $result;
}
/**
* Find objects linked with the given groupware object through a relation
*
* @param string Object UUID
* @param array List of related URIs
*/
public function get_object_links($uid)
{
$links = array();
$object_uri = self::build_member_url($uid);
foreach ($this->get_relations_for_member($uid) as $relation) {
if (in_array($object_uri, (array) $relation['members'])) {
// make relation members up-to-date
kolab_storage_config::resolve_members($relation);
foreach ($relation['members'] as $member) {
if ($member != $object_uri) {
$links[] = $member;
}
}
}
}
return array_unique($links);
}
/**
*
*/
public function save_object_links($uid, $links, $remove = array())
{
$object_uri = self::build_member_url($uid);
$relations = $this->get_relations_for_member($uid);
$done = false;
foreach ($relations as $relation) {
// make relation members up-to-date
kolab_storage_config::resolve_members($relation);
// remove and add links
$members = array_diff($relation['members'], (array)$remove);
$members = array_unique(array_merge($members, $links));
// make sure the object_uri is still a member
if (!in_array($object_uri, $members)) {
$members[$object_uri];
}
// remove relation if no other members remain
if (count($members) <= 1) {
$done = $this->delete($relation['uid']);
}
// update relation object if members changed
else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
$relation['members'] = $members;
$done = $this->save($relation, 'relation');
$links = array();
}
// no changes, we're happy
else {
$done = true;
$links = array();
}
}
// create a new relation
if (!$done && !empty($links)) {
$relation = array(
'members' => array_merge($links, array($object_uri)),
'category' => 'generic',
);
$ret = $this->save($relation, 'relation');
}
return $ret;
}
/**
* Find relation objects referring to specified note
*/
public function get_relations_for_member($uid, $reltype = 'generic')
{
$default = true;
$filter = array(
array('type', '=', 'relation'),
array('category', '=', $reltype),
array('member', '=', $uid),
);
return $this->get_objects($filter, $default, 100);
}
/**
* Find kolab objects assigned to specified e-mail message
*
* @param rcube_message $message E-mail message
* @param string $folder Folder name
* @param string $type Result objects type
*
* @return array List of kolab objects
*/
public function get_message_relations($message, $folder, $type)
{
static $_cache = array();
$result = array();
$uids = array();
$default = true;
$uri = self::get_message_uri($message, $folder);
$filter = array(
array('type', '=', 'relation'),
array('category', '=', 'generic'),
);
// query by message-id
$member_id = $message->get('message-id', false);
if (empty($member_id)) {
// derive message identifier from URI
$member_id = md5($uri);
}
$filter[] = array('member', '=', $member_id);
if (!isset($_cache[$uri])) {
// get UIDs of related groupware objects
foreach ($this->get_objects($filter, $default) as $relation) {
// we don't need to update members if the URI is found
if (!in_array($uri, $relation['members'])) {
// update members...
$messages = kolab_storage_config::resolve_members($relation);
// ...and check again
if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
continue;
}
}
// find groupware object UID(s)
foreach ($relation['members'] as $member) {
if (strpos($member, 'urn:uuid:') === 0) {
$uids[] = substr($member, 9);
}
}
}
// remember this lookup
$_cache[$uri] = $uids;
}
else {
$uids = $_cache[$uri];
}
// get kolab objects of specified type
if (!empty($uids)) {
$query = array(array('uid', '=', array_unique($uids)));
$result = kolab_storage::select($query, $type);
}
return $result;
}
/**
* Build a URI representing the given message reference
*/
public static function get_message_uri($headers, $folder)
{
$params = array(
'folder' => $headers->folder ?: $folder,
'uid' => $headers->uid,
);
if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
$params['message-id'] = $messageid;
$params['date'] = $date;
if ($subject = $headers->get('subject')) {
$params['subject'] = $subject;
}
}
return self::build_member_url($params);
}
+
+ /**
+ * Resolve the email message reference from the given URI
+ */
+ public function get_message_reference($uri, $rel = null)
+ {
+ if ($linkref = self::parse_member_url($uri)) {
+ $linkref['subject'] = $linkref['params']['subject'];
+ $linkref['uri'] = $uri;
+
+ $rcmail = rcube::get_instance();
+ if (method_exists($rcmail, 'url')) {
+ $linkref['mailurl'] = $rcmail->url(array(
+ 'task' => 'mail',
+ 'action' => 'show',
+ 'mbox' => $linkref['folder'],
+ 'uid' => $linkref['uid'],
+ 'rel' => $rel,
+ ));
+ }
+
+ unset($linkref['params']);
+ }
+
+ return $linkref;
+ }
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
index 2435fa3..ab3c63f 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
@@ -1,1077 +1,1166 @@
<?php
/**
* The kolab_storage_folder class represents an IMAP folder on the Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_folder extends kolab_storage_folder_api
{
/**
* The kolab_storage_cache instance for caching operations
* @var object
*/
public $cache;
- private $type_annotation;
- private $resource_uri;
+ /**
+ * Indicate validity status
+ * @var boolean
+ */
+ public $valid = false;
+
+ protected $error = 0;
+
+ protected $resource_uri;
/**
* Default constructor
+ *
+ * @param string The folder name/path
+ * @param string Expected folder type
*/
- function __construct($name, $type = null)
+ function __construct($name, $type = null, $type_annotation = null)
{
parent::__construct($name);
$this->imap->set_options(array('skip_deleted' => true));
- $this->set_folder($name, $type);
+ $this->set_folder($name, $type, $type_annotation);
}
/**
* Set the IMAP folder this instance connects to
*
* @param string The folder name/path
+ * @param string Expected folder type
* @param string Optional folder type if known
*/
- public function set_folder($name, $type = null)
+ public function set_folder($name, $type = null, $type_annotation = null)
{
- $this->type_annotation = $type ? $type : kolab_storage::folder_type($name);
+ if (empty($type_annotation)) {
+ $type_annotation = kolab_storage::folder_type($name);
+ }
$oldtype = $this->type;
- list($this->type, $suffix) = explode('.', $this->type_annotation);
+ list($this->type, $suffix) = explode('.', $type_annotation);
$this->default = $suffix == 'default';
$this->subtype = $this->default ? '' : $suffix;
$this->name = $name;
$this->id = kolab_storage::folder_id($name);
+ $this->valid = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);
+
+ if (!$this->valid) {
+ $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
+ }
// reset cached object properties
$this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
- // get a new cache instance of folder type changed
- if (!$this->cache || $type != $oldtype)
+ // get a new cache instance if folder type changed
+ if (!$this->cache || $this->type != $oldtype)
$this->cache = kolab_storage_cache::factory($this);
+ else
+ $this->cache->set_folder($this);
$this->imap->set_folder($this->name);
- $this->cache->set_folder($this);
}
+ /**
+ * Returns code of last error
+ *
+ * @return int Error code
+ */
+ public function get_error()
+ {
+ return $this->error ?: $this->cache->get_error();
+ }
+
+ /**
+ * Check IMAP connection error state
+ */
+ public function check_error()
+ {
+ if (($err_code = $this->imap->get_error_code()) < 0) {
+ $this->error = kolab_storage::ERROR_IMAP_CONN;
+ if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
+ $this->error = kolab_storage::ERROR_NO_PERMISSION;
+ }
+ }
+
+ return $this->error;
+ }
/**
* Compose a unique resource URI for this IMAP folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri))
return $this->resource_uri;
// strip namespace prefix from folder name
$ns = $this->get_namespace();
$nsdata = $this->imap->get_namespace($ns);
if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
$subpath = substr($this->name, strlen($nsdata[0][0]));
if ($ns == 'other') {
list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
$subpath = $suffix;
}
}
else {
$subpath = $this->name;
}
// compose fully qualified ressource uri for this instance
- $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+ $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
return $this->resource_uri;
}
/**
* Helper method to extract folder UID metadata
*
* @return string Folder's UID
*/
public function get_uid()
{
// UID is defined in folder METADATA
$metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS);
$metadata = $this->get_metadata($metakeys);
foreach ($metakeys as $key) {
if (($uid = $metadata[$key])) {
return $uid;
}
}
// generate a folder UID and set it to IMAP
$uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
if ($this->set_uid($uid)) {
return $uid;
}
// create hash from folder name if we can't write the UID metadata
return md5($this->name . $this->get_owner());
}
/**
* Helper method to set an UID value to the given IMAP folder instance
*
* @param string Folder's UID
* @return boolean True on succes, False on failure
*/
public function set_uid($uid)
{
if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) {
$success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid));
}
+
+ $this->check_error();
return $success;
}
/**
* Compose a folder Etag identifier
*/
public function get_ctag()
{
$fdata = $this->get_imap_data();
+ $this->check_error();
return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
}
/**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* Get number of objects stored in this folder
*
* @param mixed Pseudo-SQL query as list of filter parameter triplets
* or string with object type (e.g. contact, event, todo, journal, note, configuration)
* @return integer The number of objects of the given type
* @see self::select()
*/
public function count($query = null)
{
+ if (!$this->valid) {
+ return 0;
+ }
+
// synchronize cache first
$this->cache->synchronize();
return $this->cache->count($this->_prepare_query($query));
}
/**
* List all Kolab objects of the given type
*
* @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
* @return array List of Kolab data objects (each represented as hash array)
*/
public function get_objects($type = null)
{
if (!$type) $type = $this->type;
+ if (!$this->valid) {
+ return array();
+ }
+
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($type));
}
/**
* Select *some* Kolab objects matching the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: array('<colname>', '<comparator>', '<value>')
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query = array())
{
+ if (!$this->valid) {
+ return array();
+ }
+
// check query argument
- if (empty($query))
+ if (empty($query)) {
return $this->get_objects();
+ }
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($query));
}
/**
* Getter for object UIDs only
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @return array List of Kolab object UIDs
*/
public function get_uids($query = array())
{
+ if (!$this->valid) {
+ return array();
+ }
+
// synchronize caches
$this->cache->synchronize();
// fetch UIDs from cache
return $this->cache->select($this->_prepare_query($query), true);
}
/**
* Setter for ORDER BY and LIMIT parameters for cache queries
*
* @param array List of columns to order by
* @param integer Limit result set to this length
* @param integer Offset row
*/
public function set_order_and_limit($sortcols, $length = null, $offset = 0)
{
$this->cache->set_order_by($sortcols);
if ($length !== null) {
$this->cache->set_limit($length, $offset);
}
}
/**
* Helper method to sanitize query arguments
*/
private function _prepare_query($query)
{
// string equals type query
// FIXME: should not be called this way!
if (is_string($query)) {
return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
}
foreach ((array)$query as $i => $param) {
if ($param[0] == 'type' && !$this->cache->has_type_col()) {
unset($query[$i]);
}
else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
if (is_object($param[2]) && is_a($param[2], 'DateTime'))
$param[2] = $param[2]->format('U');
if (is_numeric($param[2]))
$query[$i][2] = date('Y-m-d H:i:s', $param[2]);
}
}
return $query;
}
/**
* Getter for a single Kolab object, identified by its UID
*
* @param string $uid Object UID
* @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
* Defaults to folder type
*
* @return array The Kolab object represented as hash array
*/
public function get_object($uid, $type = null)
{
+ if (!$this->valid) {
+ return false;
+ }
+
// synchronize caches
$this->cache->synchronize();
$msguid = $this->cache->uid2msguid($uid);
if ($msguid && ($object = $this->cache->get($msguid, $type))) {
return $object;
}
return false;
}
/**
* Fetch a Kolab object attachment which is stored in a separate part
* of the mail MIME message that represents the Kolab record.
*
* @param string Object's UID
* @param string The attachment's mime number
* @param string IMAP folder where message is stored;
* If set, that also implies that the given UID is an IMAP UID
* @param bool True to print the part content
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
*
* @return mixed The attachment content as binary string
*/
public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
{
- if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
+ if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
if (substr($part, 0, 2) == 'i:') {
// attachment data is stored in XML
if ($object = $this->cache->get($msguid)) {
// load data from XML (attachment content is not stored in cache)
if ($object['_formatobj'] && isset($object['_size'])) {
$object['_attachments'] = array();
$object['_formatobj']->get_attachments($object);
}
- foreach ($object['_attachments'] as $k => $attach) {
+ foreach ($object['_attachments'] as $attach) {
if ($attach['id'] == $part) {
if ($print) echo $attach['content'];
else if ($fp) fwrite($fp, $attach['content']);
else return $attach['content'];
return true;
}
}
}
}
else {
// return message part from IMAP directly
return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
}
}
return null;
}
/**
* Fetch the mime message from the storage server and extract
* the Kolab groupware object from it
*
* @param string The IMAP message UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string The folder name where the message is stored
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($msguid, $type = null, $folder = null)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if (!$type) $type = $this->type;
if (!$folder) $folder = $this->name;
$this->imap->set_folder($folder);
$this->cache->bypass(true);
$message = new rcube_message($msguid);
$this->cache->bypass(false);
// Message doesn't exist?
if (empty($message->headers)) {
return false;
}
// extract the X-Kolab-Type header from the XML attachment part if missing
if (empty($message->headers->others['x-kolab-type'])) {
foreach ((array)$message->attachments as $part) {
if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
$message->headers->others['x-kolab-type'] = $part->mimetype;
break;
}
}
}
// fix buggy messages stating the X-Kolab-Type header twice
else if (is_array($message->headers->others['x-kolab-type'])) {
$message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
}
// no object type header found: abort
if (empty($message->headers->others['x-kolab-type'])) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
), true);
return false;
}
$object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
$content_type = kolab_format::KTYPE_PREFIX . $object_type;
// check object type header and abort on mismatch
if ($type != '*' && $object_type != $type)
return false;
$attachments = array();
// get XML part
foreach ((array)$message->attachments as $part) {
if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) {
- $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
+ $xml = $message->get_part_body($part->mime_id, true);
}
else if ($part->filename || $part->content_id) {
$key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
$size = null;
// Use Content-Disposition 'size' as for the Kolab Format spec.
if (isset($part->d_parameters['size'])) {
$size = $part->d_parameters['size'];
}
// we can trust part size only if it's not encoded
else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
$size = $part->size;
}
$attachments[$key] = array(
'id' => $part->mime_id,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'size' => $size,
);
}
}
if (!$xml) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not find Kolab data part in message $msguid ($this->name).",
), true);
return false;
}
// check kolab format version
$format_version = $message->headers->others['x-kolab-mime-version'];
if (empty($format_version)) {
list($xmltype, $subtype) = explode('.', $object_type);
$xmlhead = substr($xml, 0, 512);
// detect old Kolab 2.0 format
if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
$format_version = '2.0';
else
$format_version = '3.0'; // assume 3.0
}
// get Kolab format handler for the given type
$format = kolab_format::factory($object_type, $format_version);
if (is_a($format, 'PEAR_Error'))
return false;
// load Kolab object from XML part
$format->load($xml);
if ($format->is_valid()) {
$object = $format->to_array(array('_attachments' => $attachments));
$object['_type'] = $object_type;
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
$object['_formatobj'] = $format;
return $object;
}
else {
// try to extract object UID from XML block
if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
$msgadd = " UID = " . trim(strip_tags($m[1]));
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
), true);
}
return false;
}
/**
* Save an object in this folder.
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param string $uid The UID of the old object if it existed before
* @return boolean True on success, false on error
*/
public function save(&$object, $type = null, $uid = null)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if (!$type)
$type = $this->type;
// copy attachments from old message
if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
foreach ((array)$old['_attachments'] as $key => $att) {
if (!isset($object['_attachments'][$key])) {
$object['_attachments'][$key] = $old['_attachments'][$key];
}
// unset deleted attachment entries
if ($object['_attachments'][$key] == false) {
unset($object['_attachments'][$key]);
}
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
if (!isset($object['photo']))
$object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
unset($object['_attachments'][$key]);
}
}
}
// save contact photo to attachment for Kolab2 format
if (kolab_storage::$version == '2.0' && $object['photo']) {
$attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
$object['_attachments'][$attkey] = array(
'mimetype'=> rcube_mime::image_content_type($object['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
);
}
// process attachments
if (is_array($object['_attachments'])) {
$numatt = count($object['_attachments']);
foreach ($object['_attachments'] as $key => $attachment) {
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
if (empty($attachment['content']) && !empty($attachment['data'])) {
$attachment['content'] = $attachment['data'];
unset($attachment['data'], $object['_attachments'][$key]['data']);
}
// make sure size is set, so object saved in cache contains this info
if (!isset($attachment['size'])) {
if (!empty($attachment['content'])) {
if (is_resource($attachment['content'])) {
// this need to be a seekable resource, otherwise
// fstat() failes and we're unable to determine size
// here nor in rcube_imap_generic before IMAP APPEND
$stat = fstat($attachment['content']);
$attachment['size'] = $stat ? $stat['size'] : 0;
}
else {
$attachment['size'] = strlen($attachment['content']);
}
}
else if (!empty($attachment['path'])) {
$attachment['size'] = filesize($attachment['path']);
}
$object['_attachments'][$key] = $attachment;
}
// generate unique keys (used as content-id) for attachments
if (is_numeric($key) && $key < $numatt) {
// derrive content-id from attachment file name
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
$basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii
if (!$basename) $basename = 'noname';
$cid = $basename . '.' . microtime(true) . $ext;
$object['_attachments'][$cid] = $attachment;
unset($object['_attachments'][$key]);
}
}
}
// save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
$this->save_recurrence_exceptions($object, $type);
}
// check IMAP BINARY extension support for 'file' objects
// allow configuration to workaround bug in Cyrus < 2.4.17
$rcmail = rcube::get_instance();
$binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
// generate and save object message
if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
// resolve old msguid before saving
if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
}
$result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
// update cache with new UID
if ($result) {
$old_uid = $object['_msguid'];
$object['_msguid'] = $result;
$object['_mailbox'] = $this->name;
if ($old_uid) {
// delete old message
$this->cache->bypass(true);
$this->imap->delete_message($old_uid, $object['_mailbox']);
$this->cache->bypass(false);
}
// insert/update message in cache
$this->cache->save($result, $object, $old_uid);
}
// remove temp file
if ($body_file) {
@unlink($body_file);
}
}
return $result;
}
/**
* Save recurrence exceptions as individual objects.
* The Kolab v2 format doesn't allow us to save fully embedded exception objects.
*
* @param array Hash array with event properties
* @param string Object type
*/
private function save_recurrence_exceptions(&$object, $type = null)
{
if ($object['recurrence']['EXCEPTIONS']) {
$exdates = array();
foreach ((array)$object['recurrence']['EXDATE'] as $exdate) {
$key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate);
$exdates[$key] = 1;
}
// save every exception as individual object
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
$exception['sequence'] = $object['sequence'] + 1;
if ($exception['thisandfuture']) {
$exception['recurrence'] = $object['recurrence'];
// adjust the recurrence duration of the exception
if ($object['recurrence']['COUNT']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
if ($end = $recurrence->end()) {
unset($exception['recurrence']['COUNT']);
$exception['recurrence']['UNTIL'] = $end;
}
}
// set UNTIL date if we have a thisandfuture exception
$untildate = clone $exception['start'];
$untildate->sub(new DateInterval('P1D'));
$object['recurrence']['UNTIL'] = $untildate;
unset($object['recurrence']['COUNT']);
}
else {
if (!$exdates[$exception['start']->format('Y-m-d')])
$object['recurrence']['EXDATE'][] = clone $exception['start'];
unset($exception['recurrence']);
}
unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
$this->save($exception, $type, $exception['uid']);
}
unset($object['recurrence']['EXCEPTIONS']);
}
}
/**
* Generate an object UID with the given recurrence-ID in a way that it is
* unique (the original UID is not a substring) but still recoverable.
*/
private static function recurrence_exception_uid($uid, $recurrence_id)
{
$offset = -2;
return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
}
/**
* Delete the specified object from this folder.
*
* @param mixed $object The Kolab object to delete or object UID
* @param boolean $expunge Should the folder be expunged?
*
* @return boolean True if successful, false on error
*/
public function delete($object, $expunge = true)
{
+ if (!$this->valid) {
+ return false;
+ }
+
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
$this->cache->bypass(true);
if ($msguid && $expunge) {
$success = $this->imap->delete_message($msguid, $this->name);
}
else if ($msguid) {
$success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
}
$this->cache->bypass(false);
if ($success) {
$this->cache->set($msguid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
+ if (!$this->valid) {
+ return false;
+ }
+
$this->cache->purge();
$this->cache->bypass(true);
$result = $this->imap->clear_folder($this->name);
$this->cache->bypass(false);
return $result;
}
/**
* Restore a previously deleted object
*
* @param string Object UID
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if ($msguid = $this->cache->uid2msguid($uid, true)) {
$this->cache->bypass(true);
$result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
$this->cache->bypass(false);
if ($result) {
return $msguid;
}
}
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param string IMAP folder to move object to
* @return boolean True on success, false on failure
*/
public function move($uid, $target_folder)
{
+ if (!$this->valid) {
+ return false;
+ }
+
if (is_string($target_folder))
$target_folder = kolab_storage::get_folder($target_folder);
if ($msguid = $this->cache->uid2msguid($uid)) {
$this->cache->bypass(true);
$result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
$this->cache->bypass(false);
if ($result) {
$this->cache->move($msguid, $uid, $target_folder);
return true;
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
), true);
}
}
return false;
}
/**
* Creates source of the configuration object message
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param bool $binary Enables use of binary encoding of attachment(s)
* @param string $body_file Reference to filename of message body
*
* @return mixed Message as string or array with two elements
* (one for message file path, second for message headers)
*/
private function build_message(&$object, $type, $binary, &$body_file)
{
// load old object to preserve data we don't understand/process
if (is_object($object['_formatobj']))
$format = $object['_formatobj'];
else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
$format = $old['_formatobj'];
// create new kolab_format instance
if (!$format)
$format = kolab_format::factory($type, kolab_storage::$version);
if (PEAR::isError($format))
return false;
$format->set($object);
$xml = $format->write(kolab_storage::$version);
$object['uid'] = $format->uid; // read UID from format
$object['_formatobj'] = $format;
if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
return false;
}
$mime = new Mail_mime("\r\n");
$rcmail = rcube::get_instance();
$headers = array();
$files = array();
$part_id = 1;
$encoding = $binary ? 'binary' : 'base64';
if ($user_email = $rcmail->get_user_email()) {
$headers['From'] = $user_email;
$headers['To'] = $user_email;
}
$headers['Date'] = date('r');
$headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
$headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
$headers['Subject'] = $object['uid'];
// $headers['Message-ID'] = $rcmail->gen_message_id();
$headers['User-Agent'] = $rcmail->config->get('useragent');
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
- foreach ($object['_attachments'] as $id => $attachment) {
+ foreach ($object['_attachments'] as $attachment) {
$memory += $attachment['size'];
}
// 1.33 is for base64, we need at least 4x more memory than the message size
if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
$marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
$is_file = true;
$temp_dir = unslashify($rcmail->config->get('temp_dir'));
$mime->setParam('delay_file_io', true);
}
}
$mime->headers($headers);
$mime->setTXTBody("This is a Kolab Groupware object. "
. "To view this object you will need an email client that understands the Kolab Groupware format. "
. "For a list of such email clients please visit http://www.kolab.org/\n\n");
$ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
// Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
// when APPENDing from temp file
$xml = preg_replace('/\r?\n/', "\r\n", $xml);
$mime->addAttachment($xml, // file
$ctype, // content-type
'kolab.xml', // filename
false, // is_file
'8bit', // encoding
'attachment', // disposition
RCUBE_CHARSET // charset
);
$part_id++;
// save object attachments as separate parts
foreach ((array)$object['_attachments'] as $key => $att) {
if (empty($att['content']) && !empty($att['id'])) {
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
$msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
if ($is_file) {
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
fclose($fp);
}
else {
return false;
}
}
else {
$att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
}
}
$headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
$name = !empty($att['name']) ? $att['name'] : $key;
// To store binary files we can use faster method
// without writting full message content to a temporary file but
// directly to IMAP, see rcube_imap_generic::append().
// I.e. use file handles where possible
if (!empty($att['path'])) {
if ($is_file && $binary) {
$files[] = fopen($att['path'], 'r');
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
$mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
else {
if (is_resource($att['content']) && $is_file && $binary) {
$files[] = $att['content'];
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
if (is_resource($att['content'])) {
@rewind($att['content']);
$att['content'] = stream_get_contents($att['content']);
}
$mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
$object['_attachments'][$key]['id'] = ++$part_id;
}
if (!$is_file || !empty($files)) {
$message = $mime->getMessage();
}
// parse message and build message array with
// attachment file pointers in place of file markers
if (!empty($files)) {
$message = explode($marker, $message);
$tmp = array();
foreach ($message as $msg_part) {
$tmp[] = $msg_part;
if ($file = array_shift($files)) {
$tmp[] = $file;
}
}
$message = $tmp;
}
// write complete message body into temp file
else if ($is_file) {
// use common temp dir
$body_file = tempnam($temp_dir, 'rcmMsg');
if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
$message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
}
return $message;
}
/**
* Triggers any required updates after changes within the
* folder. This is currently only required for handling free/busy
* information with Kolab.
*
* @return boolean|PEAR_Error True if successfull.
*/
public function trigger()
{
$owner = $this->get_owner();
$result = false;
switch($this->type) {
case 'event':
if ($this->get_namespace() == 'personal') {
$result = $this->trigger_url(
sprintf('%s/trigger/%s/%s.pfb',
kolab_storage::get_freebusy_server(),
urlencode($owner),
urlencode($this->imap->mod_folder($this->name))
),
$this->imap->options['user'],
$this->imap->options['password']
);
}
break;
default:
return true;
}
if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
$this->name, $result->getMessage()));
}
return $result;
}
/**
* Triggers a URL.
*
* @param string $url The URL to be triggered.
* @param string $auth_user Username to authenticate with
* @param string $auth_passwd Password for basic auth
* @return boolean|PEAR_Error True if successfull.
*/
private function trigger_url($url, $auth_user = null, $auth_passwd = null)
{
- require_once('HTTP/Request2.php');
-
try {
$request = libkolab::http_request($url);
// set authentication credentials
if ($auth_user && $auth_passwd)
$request->setAuth($auth_user, $auth_passwd);
$result = $request->send();
// rcube::write_log('trigger', $result->getBody());
}
catch (Exception $e) {
return PEAR::raiseError($e->getMessage());
}
return true;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
index ea603b1..7280389 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -1,345 +1,351 @@
<?php
/**
* Abstract interface class for Kolab storage IMAP folder objects
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_storage_folder_api
{
/**
* Folder identifier
* @var string
*/
public $id;
/**
* The folder name.
* @var string
*/
public $name;
/**
* The type of this folder.
* @var string
*/
public $type;
/**
* The subtype of this folder.
* @var string
*/
public $subtype;
/**
* Is this folder set to be the default for its type
* @var boolean
*/
public $default = false;
/**
* List of direct child folders
* @var array
*/
public $children = array();
/**
* Name of the parent folder
* @var string
*/
public $parent = '';
protected $imap;
protected $owner;
protected $info;
protected $idata;
protected $namespace;
/**
* Private constructor
*/
protected function __construct($name)
{
$this->name = $name;
$this->id = kolab_storage::folder_id($name);
$this->imap = rcube::get_instance()->get_storage();
}
/**
* Returns the owner of the folder.
*
+ * @param boolean Return a fully qualified owner name (i.e. including domain for shared folders)
* @return string The owner of this folder.
*/
- public function get_owner()
+ public function get_owner($fully_qualified = false)
{
// return cached value
if (isset($this->owner))
return $this->owner;
$info = $this->get_folder_info();
$rcmail = rcube::get_instance();
switch ($info['namespace']) {
case 'personal':
$this->owner = $rcmail->get_user_name();
break;
case 'shared':
$this->owner = 'anonymous';
break;
default:
- list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
- if (strpos($user, '@') === false) {
- $domain = strstr($rcmail->get_user_name(), '@');
- if (!empty($domain))
- $user .= $domain;
- }
- $this->owner = $user;
+ list($prefix, $this->owner) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+ $fully_qualified = true; // enforce email addresses (backwards compatibility)
break;
}
+ if ($fully_qualified && strpos($this->owner, '@') === false) {
+ // extract domain from current user name
+ $domain = strstr($rcmail->get_user_name(), '@');
+ // fall back to mail_domain config option
+ if (empty($domain) && ($mdomain = $rcmail->config->mail_domain($this->imap->options['host']))) {
+ $domain = '@' . $mdomain;
+ }
+ $this->owner .= $domain;
+ }
+
return $this->owner;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if (!isset($this->namespace))
$this->namespace = $this->imap->folder_namespace($this->name);
return $this->namespace;
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return kolab_storage::object_name($this->name, $this->get_namespace());
}
/**
* Getter for the top-end folder name (not the entire path)
*
* @return string Name of this folder
*/
public function get_foldername()
{
$parts = explode('/', $this->name);
return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
$path = explode('/', $this->name);
array_pop($path);
// don't list top-level namespace folder
if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
$path = array();
}
return join('/', $path);
}
/**
* Getter for the Cyrus mailbox identifier corresponding to this folder
* (e.g. user/john.doe/Calendar/Personal@example.org)
*
* @return string Mailbox ID
*/
public function get_mailbox_id()
{
$info = $this->get_folder_info();
$owner = $this->get_owner();
list($user, $domain) = explode('@', $owner);
switch ($info['namespace']) {
case 'personal':
return sprintf('user/%s/%s@%s', $user, $this->name, $domain);
case 'shared':
$ns = $this->imap->get_namespace('shared');
$prefix = is_array($ns) ? $ns[0][0] : '';
list(, $domain) = explode('@', rcube::get_instance()->get_user_name());
return substr($this->name, strlen($prefix)) . '@' . $domain;
default:
$ns = $this->imap->get_namespace('other');
$prefix = is_array($ns) ? $ns[0][0] : '';
list($user, $folder) = explode($this->imap->get_hierarchy_delimiter(), substr($info['name'], strlen($prefix)), 2);
if (strpos($user, '@')) {
list($user, $domain) = explode('@', $user);
}
return sprintf('user/%s/%s@%s', $user, $folder, $domain);
}
}
/**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
* @return mixed Color value from IMAP metadata or $default is not set
*/
public function get_color($default = null)
{
// color is defined in folder METADATA
$metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
return $color;
}
return $default;
}
/**
* Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
*
* @param array List of metadata keys to read
* @return array Metadata entry-value hash array on success, NULL on error
*/
public function get_metadata($keys)
{
$metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
return $metadata[$this->name];
}
/**
* Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param array $entries Entry-value array (use NULL value as NIL)
* @return boolean True on success, False on failure
*/
public function set_metadata($entries)
{
return $this->imap->set_metadata($this->name, $entries);
}
/**
*
*/
public function get_folder_info()
{
if (!isset($this->info))
$this->info = $this->imap->folder_info($this->name);
return $this->info;
}
/**
* Make IMAP folder data available for this folder
*/
public function get_imap_data()
{
if (!isset($this->idata))
$this->idata = $this->imap->folder_data($this->name);
return $this->idata;
}
/**
* Get IMAP ACL information for this folder
*
* @return string Permissions as string
*/
public function get_myrights()
{
$rights = $this->info['rights'];
if (!is_array($rights))
$rights = $this->imap->my_rights($this->name);
return join('', (array)$rights);
}
/**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* Return folder name as string representation of this object
*
* @return string Full IMAP folder name
*/
public function __toString()
{
return $this->name;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/package.xml b/lib/drivers/kolab/plugins/libkolab/package.xml
deleted file mode 100644
index cd3e3a0..0000000
--- a/lib/drivers/kolab/plugins/libkolab/package.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
- http://pear.php.net/dtd/tasks-1.0.xsd
- http://pear.php.net/dtd/package-2.0
- http://pear.php.net/dtd/package-2.0.xsd">
- <name>libkolab</name>
- <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
- <summary>Kolab core library</summary>
- <description>Plugin to setup a basic environment for the interaction with a Kolab server.</description>
- <lead>
- <name>Thomas Bruederli</name>
- <user>bruederli</user>
- <email>bruederli@kolabsys.com</email>
- <active>yes</active>
- </lead>
- <developer>
- <name>Alensader Machniak</name>
- <user>machniak</user>
- <email>machniak@kolabsys.com</email>
- <active>yes</active>
- </developer>
- <date>2013-04-19</date>
- <version>
- <release>0.9</release>
- <api>0.9</api>
- </version>
- <stability>
- <release>stable</release>
- <api>stable</api>
- </stability>
- <license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
- <notes>-</notes>
- <contents>
- <dir baseinstalldir="/" name="/">
- <file name="libkolab.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_configuration.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_contact.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_distributionlist.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_event.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_file.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_journal.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_note.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_task.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_format_xcal.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_storage.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_storage_cache.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_storage_folder.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
- <file name="lib/kolab_date_recurrence.php" role="php">
- <tasks:replace from="@package_version@" to="version" type="package-info"/>
- </file>
-
- <file name="bin/modcache.php" role="php"></file>
-
- <file name="config.inc.php.dist" role="data"></file>
- <file name="LICENSE" role="data"></file>
- <file name="README" role="data"></file>
- <file name="UPGRADING" role="data"></file>
- </dir>
- <!-- / -->
- </contents>
- <dependencies>
- <required>
- <php>
- <min>5.3.1</min>
- </php>
- <pearinstaller>
- <min>1.7.0</min>
- </pearinstaller>
- </required>
- </dependencies>
- <phprelease/>
-</package>

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 12:28 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
197094
Default Alt Text
(348 KB)

Event Timeline