Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2522348
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
222 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/plugins/kolab_auth/kolab_auth.php b/lib/plugins/kolab_auth/kolab_auth.php
index 5579743..b139e32 100644
--- a/lib/plugins/kolab_auth/kolab_auth.php
+++ b/lib/plugins/kolab_auth/kolab_auth.php
@@ -1,487 +1,509 @@
<?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-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_auth extends rcube_plugin
{
static $ldap;
private $data = array();
public function init()
{
$rcmail = rcube::get_instance();
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('user_create', array($this, 'user_create'));
// 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('write_log', array($this, 'write_log'));
// TODO: This section does not actually seem to work
if ($rcmail->config->get('kolab_auth_auditlog', false)) {
$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('sql_debug', 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);
}
}
public function startup($args)
{
// Arguments are task / action, not interested
if (!empty($_SESSION['user_roledns'])) {
$this->load_user_role_plugins_and_settings($_SESSION['user_roledns']);
}
return $args;
}
public function load_user_role_plugins_and_settings($role_dns)
{
$rcmail = rcube::get_instance();
$this->load_config();
// Check role dependent plugins to enable and settings to modify
// 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');
+ $ldap = self::ldap();
+ if (!$ldap || !$ldap->ready) {
+ $args['abort'] = true;
+ return $args;
+ }
+
+ if (!empty($role_plugins)) {
+ foreach ($role_plugins as $role_dn => $plugins) {
+ $role_plugins[$ldap->parse_vars($role_dn)] = $plugins;
+ }
+ }
+
+ if (!empty($role_settings)) {
+ foreach ($role_settings as $role_dn => $settings) {
+ $role_settings[$ldap->parse_vars($role_dn)] = $settings;
+ }
+ }
+
foreach ($role_dns 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])) {
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']) {
$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);
}
}
}
}
}
}
public function write_log($args)
{
$rcmail = rcube::get_instance();
if (!$rcmail->config->get('kolab_auth_auditlog', false)) {
return $args;
}
$args['abort'] = true;
if ($rcmail->config->get('log_driver') == 'syslog') {
$prio = $args['name'] == 'errors' ? LOG_ERR : LOG_INFO;
syslog($prio, $args['line']);
return $args;
}
else {
$line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
// log_driver == 'file' is assumed here
$log_dir = $rcmail->config->get('log_dir', 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)) {
// Attempt to create the directory
if (@mkdir($log_path, 0750, true)) {
$log_dir = $log_path;
}
}
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);
return $args;
}
else {
trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
}
}
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->load_config();
$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;
}
$ldap = self::ldap();
if (!$ldap || !$ldap->ready) {
$args['abort'] = true;
return $args;
}
// Find user record in LDAP
$record = $ldap->get_user_record($user, $host);
if (empty($record)) {
$args['abort'] = true;
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');
if (!empty($role_attr) && !empty($record[$role_attr])) {
$_SESSION['user_roledns'] = (array)($record[$role_attr]);
}
// Login As...
if (!empty($loginas) && $admin_login) {
// Authenticate to LDAP
$result = $ldap->bind($record['dn'], $pass);
if (!$result) {
$args['abort'] = true;
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;
}
}
// check group
if (!$isadmin && !empty($group)) {
$groups = $ldap->get_user_groups($record['dn'], $user, $host);
if (in_array($group, $groups)) {
$isadmin = true;
}
}
// 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 (empty($record)) {
$args['abort'] = true;
return $args;
}
$args['user'] = $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);
}
// 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'];
// 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'];
}
// 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()));
}
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;
}
return $args;
}
/**
* Initializes LDAP object and connects to LDAP server
*/
public static function ldap()
{
if (self::$ldap) {
return self::$ldap;
}
$rcmail = rcube::get_instance();
// $this->load_config();
// we're in static method, load config manually
$fpath = $rcmail->plugins->dir . '/kolab_auth/config.inc.php';
if (is_file($fpath) && !$rcmail->config->load_from_file($fpath)) {
rcube::raise_error(array(
'code' => 527, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load config from $fpath"), true, false);
}
$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;
}
}
diff --git a/lib/plugins/kolab_auth/kolab_auth_ldap.php b/lib/plugins/kolab_auth/kolab_auth_ldap.php
index c8ebcd1..4584c60 100644
--- a/lib/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/lib/plugins/kolab_auth/kolab_auth_ldap.php
@@ -1,489 +1,485 @@
<?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();
function __construct($p)
{
$rcmail = rcube::get_instance();
$this->debug = (bool) $rcmail->config->get('ldap_debug');
$this->fieldmap = $p['fieldmap'];
$this->fieldmap['uid'] = 'uid';
$p['attributes'] = array_values($this->fieldmap);
// 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) {
$entry = rcube_ldap_generic::normalize_entry($entry);
if (!$entry['dn']) {
$entry['dn'] = $result->get_dn();
}
$groups[$entry['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]);
}
}
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 $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
*
* @return array List or false on error
*/
function search($fields, $value, $mode=1, $required = array(), $limit = 0)
{
$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]) {
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]) {
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)) {
$i = 0;
foreach ($result as $entry) {
if ($limit && $limit <= $i) {
break;
}
$dn = $result->get_dn();
$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[$field] = $entry[$attr];
}
}
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'];
}
else 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 && ($r_domain = $this->find_domain($dom))) {
- // $dom is a domain DN string?
- if (strpos($r_domain, '=')) {
- $dc = $r_domain;
- }
- else {
- $user = $usr . '@' . $r_domain;
- }
+ if ($dom != $domain && ($dc = $this->find_domain($dom))) {
+ // @FIXME: we should replace domain in $user, I suppose
}
}
- else if ($domain && !strpos($user, '@')) {
+ 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);
return strtr($str, $replaces);
}
/**
* Find root domain for specified domain
*
* @param string $domain Domain name
*
- * @return string Domain name or domain DN string
+ * @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'];
}
- return is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+ $domain = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+
+ return $domain ? 'dc=' . implode(',dc=', explode('.', $domain)) : null;
}
}
/**
* 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/plugins/kolab_folders/kolab_folders.php b/lib/plugins/kolab_folders/kolab_folders.php
index 5b1d1d3..cc43390 100644
--- a/lib/plugins/kolab_folders/kolab_folders.php
+++ b/lib/plugins/kolab_folders/kolab_folders.php
@@ -1,561 +1,562 @@
<?php
/**
* Type-aware folder management/listing for Kolab
*
* @version @package_version@
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011, 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_folders extends rcube_plugin
{
public $task = '?(?!login).*';
public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy');
public $mail_types = array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail');
private $rc;
private static $instance;
/**
* Plugin initialization.
*/
function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
// Folder listing hooks
$this->add_hook('storage_folders', array($this, 'mailboxes_list'));
// Folder manager hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
$this->add_hook('folder_update', array($this, 'folder_save'));
$this->add_hook('folder_create', array($this, 'folder_save'));
$this->add_hook('folder_delete', array($this, 'folder_save'));
$this->add_hook('folder_rename', array($this, 'folder_save'));
$this->add_hook('folders_list', array($this, 'folders_list'));
// Special folders setting
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
/**
* Handler for mailboxes_list hook. Enables type-aware lists filtering.
*/
function mailboxes_list($args)
{
// infinite loop prevention
if ($this->is_processing) {
return $args;
}
if (!$this->metadata_support()) {
return $args;
}
$this->is_processing = true;
// get folders
$folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB', $folderdata);
$this->is_processing = false;
if (!is_array($folders)) {
return $args;
}
// Create default folders
if ($args['root'] == '' && $args['name'] = '*') {
$this->create_default_folders($folders, $args['filter'], $folderdata);
}
$args['folders'] = $folders;
return $args;
}
/**
* Handler for folders_list hook. Add css classes to folder rows.
*/
function folders_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
// get folders types
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return $args;
}
$table = $args['table'];
// Add type-based style for table rows
// See kolab_folders::folder_class_name()
for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
$attrib = $table->get_row_attribs($i);
$folder = $attrib['foldername']; // UTF7-IMAP
$type = $folderdata[$folder];
if (!$type) {
$type = 'mail';
}
$class_name = self::folder_class_name($type);
$attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
$table->set_row_attribs($attrib, $i);
}
return $args;
}
/**
* Handler for folder info/edit form (folder_form hook).
* Adds folder type selector.
*/
function folder_form($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// INBOX folder is of type mail.inbox and this cannot be changed
if ($args['name'] == 'INBOX') {
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
);
return $args;
}
if ($args['options']['is_root']) {
return $args;
}
$mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
if (isset($_POST['_ctype'])) {
$new_ctype = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
$new_subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
}
// Get type of the folder or the parent
if (strlen($mbox)) {
list($ctype, $subtype) = $this->get_folder_type($mbox);
if (strlen($args['parent_name']) && $subtype == 'default')
$subtype = ''; // there can be only one
}
if (!$ctype) {
$ctype = 'mail';
}
$storage = $this->rc->get_storage();
// Don't allow changing type of shared folder, according to ACL
if (strlen($mbox)) {
$options = $storage->folder_info($mbox);
if ($options['namespace'] != 'personal' && !in_array('a', (array)$options['rights'])) {
if (in_array($ctype, $this->types)) {
$value = $this->gettext('foldertype'.$ctype);
}
else {
$value = $ctype;
}
if ($subtype) {
$value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
}
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $value,
);
return $args;
}
}
// Add javascript script to the client
$this->include_script('kolab_folders.js');
// build type SELECT fields
$type_select = new html_select(array('name' => '_ctype', 'id' => '_ctype'));
$sub_select = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
foreach ($this->types as $type) {
$type_select->add($this->gettext('foldertype'.$type), $type);
}
// add non-supported type
if (!in_array($ctype, $this->types)) {
$type_select->add($ctype, $ctype);
}
$sub_select->add('', '');
$sub_select->add($this->gettext('default'), 'default');
foreach ($this->mail_types as $type) {
$sub_select->add($this->gettext($type), $type);
}
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $type_select->show(isset($new_ctype) ? $new_ctype : $ctype)
. $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype),
);
return $args;
}
/**
* Handler for folder update/create action (folder_update/folder_create hook).
*/
function folder_save($args)
{
// Folder actions from folders list
if (empty($args['record'])) {
return $args;
}
// Folder create/update with form
$ctype = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
$subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
$mbox = $args['record']['name'];
$old_mbox = $args['record']['oldname'];
$subscribe = $args['record']['subscribe'];
if (empty($ctype)) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Skip folder creation/rename in core
// @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
// Using create_mailbox/rename_mailbox here looks bad
$args['abort'] = true;
// There can be only one default folder of specified type
if ($subtype == 'default') {
$default = $this->get_default_folder($ctype);
if ($default !== null && $old_mbox != $default) {
$args['result'] = false;
$args['message'] = $this->gettext('defaultfolderexists');
return $args;
}
}
// Subtype sanity-checks
else if ($subtype && ($ctype != 'mail' || !in_array($subtype, $this->mail_types))) {
$subtype = '';
}
$ctype .= $subtype ? '.'.$subtype : '';
$storage = $this->rc->get_storage();
// Create folder
if (!strlen($old_mbox)) {
// By default don't subscribe to non-mail folders
if ($subscribe)
$subscribe = (bool) preg_match('/^mail/', $ctype);
$result = $storage->create_folder($mbox, $subscribe);
// Set folder type
if ($result) {
$this->set_folder_type($mbox, $ctype);
}
}
// Rename folder
else {
if ($old_mbox != $mbox) {
$result = $storage->rename_folder($old_mbox, $mbox);
}
else {
$result = true;
}
if ($result) {
list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
$oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
if ($ctype != $oldtype) {
$this->set_folder_type($mbox, $ctype);
}
}
}
$args['record']['class'] = self::folder_class_name($ctype);
$args['record']['subscribe'] = $subscribe;
$args['result'] = $result;
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'folders') {
return $args;
}
// Load configuration
$this->load_config();
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
// special handling for 'default_folders'
if (in_array('default_folders', $dont_override)) {
return $args;
}
// map config option name to kolab folder type annotation
$opts = array(
'drafts_mbox' => 'mail.drafts',
'sent_mbox' => 'mail.sentitems',
'junk_mbox' => 'mail.junkemail',
'trash_mbox' => 'mail.wastebasket',
);
// check if any of special folders has been changed
foreach ($opts as $opt_name => $type) {
$new = $args['prefs'][$opt_name];
$old = $this->rc->config->get($opt_name);
if ($new === $old) {
unset($opts[$opt_name]);
}
}
if (empty($opts)) {
return $args;
}
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return $args;
}
foreach ($opts as $opt_name => $type) {
$foldername = $args['prefs'][$opt_name];
if (strlen($foldername)) {
// get all folders of specified type
$folders = array_intersect($folderdata, array($type));
// folder already annotated with specified type
if (!empty($folders[$foldername])) {
continue;
}
// set type to the new folder
$this->set_folder_type($foldername, $type);
// unset old folder(s) type annotation
list($maintype, $subtype) = explode('.', $type);
foreach (array_keys($folders) as $folder) {
$this->set_folder_type($folder, $maintype);
}
}
}
return $args;
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @return boolean
*/
function metadata_support()
{
$storage = $this->rc->get_storage();
return $storage->get_capability('METADATA') ||
$storage->get_capability('ANNOTATEMORE') ||
$storage->get_capability('ANNOTATEMORE2');
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @param string $folder Folder name
*
* @return array Folder content-type
*/
function get_folder_type($folder)
{
return explode('.', (string)kolab_storage::folder_type($folder));
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
function set_folder_type($folder, $type = 'mail')
{
return kolab_storage::set_folder_type($folder, $type);
}
/**
* Returns the name of default folder
*
* @param string $type Folder type
*
* @return string Folder name
*/
function get_default_folder($type)
{
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return null;
}
// get all folders of specified type
$folderdata = array_intersect($folderdata, array($type.'.default'));
return key($folderdata);
}
/**
* Returns CSS class name for specified folder type
*
* @param string $type Folder type
*
* @return string Class name
*/
static function folder_class_name($type)
{
list($ctype, $subtype) = explode('.', $type);
$class[] = 'type-' . ($ctype ? $ctype : 'mail');
if ($subtype)
$class[] = 'subtype-' . $subtype;
return implode(' ', $class);
}
/**
* Creates default folders if they doesn't exist
*/
private function create_default_folders(&$folders, $filter, $folderdata = null)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
$defaults = array();
$prefix = '';
// Find personal namespace prefix
if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
$prefix = $namespace['personal'][0][0];
}
$this->load_config();
// get configured defaults
foreach ($this->types as $type) {
$subtypes = $type == 'mail' ? $this->mail_types : array('default');
foreach ($subtypes as $subtype) {
$opt_name = 'kolab_folders_' . $type . '_' . $subtype;
if ($folder = $this->rc->config->get($opt_name)) {
// convert configuration value to UTF7-IMAP charset
$folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
// and namespace prefix if needed
if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
$defaults[$type . '.' . $subtype] = $folder;
}
}
}
if (empty($defaults)) {
return;
}
if ($folderdata === null) {
$folderdata = kolab_storage::folders_typedata();
}
if (!is_array($folderdata)) {
return;
}
// find default folders
foreach ($defaults as $type => $foldername) {
// get all folders of specified type
$_folders = array_intersect($folderdata, array($type));
// default folder found
if (!empty($_folders)) {
continue;
}
list($type1, $type2) = explode('.', $type);
$exists = !empty($folderdata[$foldername]) || $foldername == 'INBOX';
// create folder
if (!$exists && !$storage->folder_exists($foldername)) {
$storage->create_folder($foldername, $type1 == 'mail');
+ $storage->subscribe($foldername);
}
// set type
$result = $this->set_folder_type($foldername, $type);
// add new folder to the result
if ($result && (!$filter || $filter == $type1)) {
$folders[] = $foldername;
}
}
}
/**
* Static getter for default folder of the given type
*
* @param string $type Folder type
* @return string Folder name
*/
public static function default_folder($type)
{
return self::$instance->get_default_folder($type);
}
}
diff --git a/lib/plugins/libkolab/SQL/postgres.initial.sql b/lib/plugins/libkolab/SQL/postgres.initial.sql
new file mode 100644
index 0000000..e06346c
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/postgres.initial.sql
@@ -0,0 +1,31 @@
+/**
+ * libkolab database schema
+ *
+ * @version @package_version@
+ * @author Sidlyarenko Sergey
+ * @licence GNU AGPL
+ **/
+
+DROP TABLE IF EXISTS kolab_cache;
+
+CREATE TABLE kolab_cache (
+ resource character varying(255) NOT NULL,
+ type character varying(32) NOT NULL,
+ msguid NUMERIC(20) NOT NULL,
+ uid character varying(128) NOT NULL,
+ created timestamp without time zone DEFAULT NULL,
+ changed timestamp without time zone DEFAULT NULL,
+ data text NOT NULL,
+ xml text NOT NULL,
+ dtstart timestamp without time zone,
+ dtend timestamp without time zone,
+ tags character varying(255) NOT NULL,
+ words text NOT NULL,
+ filename character varying(255) DEFAULT NULL,
+ PRIMARY KEY(resource, type, msguid)
+);
+
+CREATE INDEX kolab_cache_resource_filename_idx ON kolab_cache (resource, filename);
+
+
+INSERT INTO system (name, value) VALUES ('libkolab-version', '2013041900');
diff --git a/lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff b/lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff
deleted file mode 100644
index e8b767d..0000000
--- a/lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff
+++ /dev/null
@@ -1,325 +0,0 @@
---- Date/Recurrence.php.orig 2012-07-10 19:54:48.000000000 +0200
-+++ Date/Recurrence.php 2012-07-10 19:55:38.000000000 +0200
-@@ -95,6 +95,20 @@
- public $recurData = null;
-
- /**
-+ * BYDAY recurrence number
-+ *
-+ * @var integer
-+ */
-+ public $recurNthDay = null;
-+
-+ /**
-+ * BYMONTH recurrence data
-+ *
-+ * @var array
-+ */
-+ public $recurMonths = array();
-+
-+ /**
- * All the exceptions from recurrence for this event.
- *
- * @var array
-@@ -157,6 +171,44 @@
- }
-
- /**
-+ *
-+ * @param integer $nthDay The nth weekday of month to repeat events on
-+ */
-+ public function setRecurNthWeekday($nth)
-+ {
-+ $this->recurNthDay = (int)$nth;
-+ }
-+
-+ /**
-+ *
-+ * @return integer The nth weekday of month to repeat events.
-+ */
-+ public function getRecurNthWeekday()
-+ {
-+ return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
-+ }
-+
-+ /**
-+ * Specifies the months for yearly (weekday) recurrence
-+ *
-+ * @param array $months List of months (integers) this event recurs on.
-+ */
-+ function setRecurByMonth($months)
-+ {
-+ $this->recurMonths = (array)$months;
-+ }
-+
-+ /**
-+ * Returns a list of months this yearly event recurs on
-+ *
-+ * @return array List of months (integers) this event recurs on.
-+ */
-+ function getRecurByMonth()
-+ {
-+ return $this->recurMonths;
-+ }
-+
-+ /**
- * Returns the days this event recurs on.
- *
- * @return integer A mask consisting of Horde_Date::MASK_* constants
-@@ -546,8 +598,13 @@
- $estart = clone $this->start;
-
- // What day of the week, and week of the month, do we recur on?
-- $nth = ceil($this->start->mday / 7);
-- $weekday = $estart->dayOfWeek();
-+ if (isset($this->recurNthDay)) {
-+ $nth = $this->recurNthDay;
-+ $weekday = log($this->recurData, 2);
-+ } else {
-+ $nth = ceil($this->start->mday / 7);
-+ $weekday = $estart->dayOfWeek();
-+ }
-
- // Adjust $estart to be the first candidate.
- $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
-@@ -660,8 +717,13 @@
- $estart = clone $this->start;
-
- // What day of the week, and week of the month, do we recur on?
-- $nth = ceil($this->start->mday / 7);
-- $weekday = $estart->dayOfWeek();
-+ if (isset($this->recurNthDay)) {
-+ $nth = $this->recurNthDay;
-+ $weekday = log($this->recurData, 2);
-+ } else {
-+ $nth = ceil($this->start->mday / 7);
-+ $weekday = $estart->dayOfWeek();
-+ }
-
- // Adjust $estart to be the first candidate.
- $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
-@@ -894,15 +956,6 @@
- case 'W':
- $this->setRecurType(self::RECUR_WEEKLY);
- if (!empty($remainder)) {
-- $maskdays = array(
-- 'SU' => Horde_Date::MASK_SUNDAY,
-- 'MO' => Horde_Date::MASK_MONDAY,
-- 'TU' => Horde_Date::MASK_TUESDAY,
-- 'WE' => Horde_Date::MASK_WEDNESDAY,
-- 'TH' => Horde_Date::MASK_THURSDAY,
-- 'FR' => Horde_Date::MASK_FRIDAY,
-- 'SA' => Horde_Date::MASK_SATURDAY,
-- );
- $mask = 0;
- while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
- $day = trim($matches[0]);
-@@ -953,7 +1006,10 @@
- list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
- $this->setRecurEnd(new Horde_Date(array('year' => $year,
- 'month' => $month,
-- 'mday' => $mday)));
-+ 'mday' => $mday,
-+ 'hour' => 23,
-+ 'min' => 59,
-+ 'sec' => 59)));
- }
- }
- }
-@@ -1049,6 +1105,16 @@
- // Always default the recurInterval to 1.
- $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
-
-+ $maskdays = array(
-+ 'SU' => Horde_Date::MASK_SUNDAY,
-+ 'MO' => Horde_Date::MASK_MONDAY,
-+ 'TU' => Horde_Date::MASK_TUESDAY,
-+ 'WE' => Horde_Date::MASK_WEDNESDAY,
-+ 'TH' => Horde_Date::MASK_THURSDAY,
-+ 'FR' => Horde_Date::MASK_FRIDAY,
-+ 'SA' => Horde_Date::MASK_SATURDAY,
-+ );
-+
- switch (Horde_String::upper($rdata['FREQ'])) {
- case 'DAILY':
- $this->setRecurType(self::RECUR_DAILY);
-@@ -1057,15 +1123,6 @@
- case 'WEEKLY':
- $this->setRecurType(self::RECUR_WEEKLY);
- if (isset($rdata['BYDAY'])) {
-- $maskdays = array(
-- 'SU' => Horde_Date::MASK_SUNDAY,
-- 'MO' => Horde_Date::MASK_MONDAY,
-- 'TU' => Horde_Date::MASK_TUESDAY,
-- 'WE' => Horde_Date::MASK_WEDNESDAY,
-- 'TH' => Horde_Date::MASK_THURSDAY,
-- 'FR' => Horde_Date::MASK_FRIDAY,
-- 'SA' => Horde_Date::MASK_SATURDAY,
-- );
- $days = explode(',', $rdata['BYDAY']);
- $mask = 0;
- foreach ($days as $day) {
-@@ -1090,6 +1147,10 @@
- case 'MONTHLY':
- if (isset($rdata['BYDAY'])) {
- $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
-+ if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
-+ $this->setRecurOnDay($maskdays[$m[2]]);
-+ $this->setRecurNthWeekday($m[1]);
-+ }
- } else {
- $this->setRecurType(self::RECUR_MONTHLY_DATE);
- }
-@@ -1100,6 +1161,14 @@
- $this->setRecurType(self::RECUR_YEARLY_DAY);
- } elseif (isset($rdata['BYDAY'])) {
- $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
-+ if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
-+ $this->setRecurOnDay($maskdays[$m[2]]);
-+ $this->setRecurNthWeekday($m[1]);
-+ }
-+ if ($rdata['BYMONTH']) {
-+ $months = explode(',', $rdata['BYMONTH']);
-+ $this->setRecurByMonth($months);
-+ }
- } else {
- $this->setRecurType(self::RECUR_YEARLY_DATE);
- }
-@@ -1163,13 +1232,19 @@
- break;
-
- case self::RECUR_MONTHLY_WEEKDAY:
-- $nth_weekday = (int)($this->start->mday / 7);
-- if (($this->start->mday % 7) > 0) {
-- $nth_weekday++;
-+ if (isset($this->recurNthDay)) {
-+ $nth_weekday = $this->recurNthDay;
-+ $day_of_week = log($this->recurData, 2);
-+ } else {
-+ $day_of_week = $this->start->dayOfWeek();
-+ $nth_weekday = (int)($this->start->mday / 7);
-+ if (($this->start->mday % 7) > 0) {
-+ $nth_weekday++;
-+ }
- }
- $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
- $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
-- . ';BYDAY=' . $nth_weekday . $vcaldays[$this->start->dayOfWeek()];
-+ . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
- break;
-
- case self::RECUR_YEARLY_DATE:
-@@ -1182,15 +1257,22 @@
- break;
-
- case self::RECUR_YEARLY_WEEKDAY:
-- $nth_weekday = (int)($this->start->mday / 7);
-- if (($this->start->mday % 7) > 0) {
-- $nth_weekday++;
-- }
-+ if (isset($this->recurNthDay)) {
-+ $nth_weekday = $this->recurNthDay;
-+ $day_of_week = log($this->recurData, 2);
-+ } else {
-+ $day_of_week = $this->start->dayOfWeek();
-+ $nth_weekday = (int)($this->start->mday / 7);
-+ if (($this->start->mday % 7) > 0) {
-+ $nth_weekday++;
-+ }
-+ }
-+ $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
- $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
- $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
- . ';BYDAY='
- . $nth_weekday
-- . $vcaldays[$this->start->dayOfWeek()]
-+ . $vcaldays[$day_of_week]
- . ';BYMONTH=' . $this->start->month;
- break;
- }
-@@ -1223,6 +1305,21 @@
-
- $this->setRecurInterval((int)$hash['interval']);
-
-+ $month2number = array(
-+ 'january' => 1,
-+ 'february' => 2,
-+ 'march' => 3,
-+ 'april' => 4,
-+ 'may' => 5,
-+ 'june' => 6,
-+ 'july' => 7,
-+ 'august' => 8,
-+ 'september' => 9,
-+ 'october' => 10,
-+ 'november' => 11,
-+ 'december' => 12,
-+ );
-+
- $parse_day = false;
- $set_daymask = false;
- $update_month = false;
-@@ -1255,11 +1352,9 @@
-
- case 'weekday':
- $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
-- $nth_weekday = (int)$hash['daynumber'];
-- $hash['daynumber'] = 1;
-+ $this->setRecurNthWeekday($hash['daynumber']);
- $parse_day = true;
-- $update_daynumber = true;
-- $update_weekday = true;
-+ $set_daymask = true;
- break;
- }
- break;
-@@ -1297,12 +1392,13 @@
- }
-
- $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
-- $nth_weekday = (int)$hash['daynumber'];
-- $hash['daynumber'] = 1;
-+ $this->setRecurNthWeekday($hash['daynumber']);
- $parse_day = true;
-- $update_month = true;
-- $update_daynumber = true;
-- $update_weekday = true;
-+ $set_daymask = true;
-+
-+ if ($hash['month'] && isset($month2number[$hash['month']])) {
-+ $this->setRecurByMonth($month2number[$hash['month']]);
-+ }
- break;
- }
- }
-@@ -1368,21 +1464,6 @@
-
- if ($update_month || $update_daynumber || $update_weekday) {
- if ($update_month) {
-- $month2number = array(
-- 'january' => 1,
-- 'february' => 2,
-- 'march' => 3,
-- 'april' => 4,
-- 'may' => 5,
-- 'june' => 6,
-- 'july' => 7,
-- 'august' => 8,
-- 'september' => 9,
-- 'october' => 10,
-- 'november' => 11,
-- 'december' => 12,
-- );
--
- if (isset($month2number[$hash['month']])) {
- $this->start->month = $month2number[$hash['month']];
- }
-@@ -1398,7 +1479,7 @@
- }
-
- if ($update_weekday) {
-- $this->start->setNthWeekday($last_found_day, $nth_weekday);
-+ $this->setNthWeekday($nth_weekday);
- }
- }
-
diff --git a/lib/plugins/libkolab/bin/Date_last_weekday.diff b/lib/plugins/libkolab/bin/Date_last_weekday.diff
deleted file mode 100644
index d260360..0000000
--- a/lib/plugins/libkolab/bin/Date_last_weekday.diff
+++ /dev/null
@@ -1,37 +0,0 @@
---- Date.php.orig 2012-07-10 19:14:26.000000000 +0200
-+++ Date.php 2012-07-10 19:16:22.000000000 +0200
-@@ -627,16 +627,25 @@
- return;
- }
-
-- $this->_mday = 1;
-- $first = $this->dayOfWeek();
-- if ($weekday < $first) {
-- $this->_mday = 8 + $weekday - $first;
-- } else {
-- $this->_mday = $weekday - $first + 1;
-+ if ($nth < 0) { // last $weekday of month
-+ $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-+ $last = $this->dayOfWeek();
-+ $this->_mday += ($weekday - $last);
-+ if ($this->_mday > $lastday)
-+ $this->_mday -= 7;
-+ }
-+ else {
-+ $this->_mday = 1;
-+ $first = $this->dayOfWeek();
-+ if ($weekday < $first) {
-+ $this->_mday = 8 + $weekday - $first;
-+ } else {
-+ $this->_mday = $weekday - $first + 1;
-+ }
-+ $diff = 7 * $nth - 7;
-+ $this->_mday += $diff;
-+ $this->_correct(self::MASK_DAY, $diff < 0);
- }
-- $diff = 7 * $nth - 7;
-- $this->_mday += $diff;
-- $this->_correct(self::MASK_DAY, $diff < 0);
- }
-
- /**
diff --git a/lib/plugins/libkolab/bin/get_horde_date.sh b/lib/plugins/libkolab/bin/get_horde_date.sh
deleted file mode 100755
index b8e663d..0000000
--- a/lib/plugins/libkolab/bin/get_horde_date.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/sh
-
-# Copy Horde_Date_Recurrence classes and dependencies to the given target directory.
-# This will create a standalone copy of the classes requried for date recurrence computation.
-
-SRCDIR=$1
-DESTDIR=$2
-BINDIR=`dirname $0`
-
-if [ ! -d "$SRCDIR" -o ! -d "$DESTDIR" ]; then
- echo "Usage: get_horde_date.sh SRCDIR DESTDIR"
- echo "Please enter valid source and destination directories for the Horde libs"
- exit 1
-fi
-
-
-# concat Date.php and Date/Utils.php
-HORDE_DATE="$DESTDIR/Horde_Date.php"
-
-echo "<?php
-
-/**
- * This is a concatenated copy of the following files:
- * Horde/Date.php, Horde/Date/Utils.php
- * Pull the latest version of these files from the PEAR channel of the Horde
- * project at http://pear.horde.org by installing the Horde_Date package.
- */
-" > $HORDE_DATE
-
-patch $SRCDIR/Date.php $BINDIR/Date_last_weekday.diff --output=$HORDE_DATE.patched
-sed 's/<?php//; s/?>//' $HORDE_DATE.patched >> $HORDE_DATE
-sed 's/<?php//; s/?>//' $SRCDIR/Date/Utils.php >> $HORDE_DATE
-
-# copy and patch Date/Recurrence.php
-HORDE_DATE_RECURRENCE="$DESTDIR/Horde_Date_Recurrence.php"
-
-echo "<?php
-
-/**
- * This is a modified copy of Horde/Date/Recurrence.php
- * Pull the latest version of this file from the PEAR channel of the Horde
- * project at http://pear.horde.org by installing the Horde_Date package.
- */
-
-if (!class_exists('Horde_Date'))
- require_once(dirname(__FILE__) . '/Horde_Date.php');
-
-// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
-class Horde_Date_Translation
-{
- function t(\$arg) { return \$arg; }
- function ngettext(\$sing, \$plur, \$num) { return (\$num > 1 ? \$plur : \$sing); }
-}
-" > $HORDE_DATE_RECURRENCE
-
-patch $SRCDIR/Date/Recurrence.php $BINDIR/Date_Recurrence_weekday.diff --output=$HORDE_DATE_RECURRENCE.patched
-sed 's/<?php//; s/?>//' $HORDE_DATE_RECURRENCE.patched >> $HORDE_DATE_RECURRENCE
-
-# remove dependency to Horde_String
-sed -i '' "s/Horde_String::/strto/" $HORDE_DATE_RECURRENCE
-
-rm $DESTDIR/Horde_Date*.patched
-
-
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
index 04d36a5..5ac9a21 100755
--- a/lib/plugins/libkolab/bin/modcache.sh
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -1,191 +1,191 @@
#!/usr/bin/env php -d enable_dl=On
<?php
/**
* Kolab storage cache modification script
*
* @version 3.0
* @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/>.
*/
define('INSTALL_PATH', realpath('.') . '/' );
ini_set('display_errors', 1);
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: modcache.sh [OPTIONS] ACTION [USERNAME ARGS ...]\n";
print "Possible actions are: expunge, clear, prewarm\n";
print "-a, --all Clear/expunge all caches\n";
print "-h, --host IMAP host name\n";
print "-u, --user IMAP user name to authenticate\n";
print "-t, --type Object types to clear/expunge cache\n";
print "-l, --limit Limit the number of records to be expunged\n";
}
// read arguments
$opts = get_opt(array(
'a' => 'all',
'h' => 'host',
'u' => 'user',
'p' => 'password',
't' => 'type',
'l' => 'limit',
'v' => 'verbose',
));
$opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
$action = $opts[0];
$rcmail = rcube::get_instance();
/*
* Script controller
*/
switch (strtolower($action)) {
/*
* Clear/expunge all cache records
*/
case 'expunge':
$expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
$sql_add = " AND created <= '" . date('Y-m-d 00:00:00', $expire) . "'";
if ($opts['limit']) {
$sql_add .= ' LIMIT ' . intval($opts['limit']);
}
case 'clear':
// connect to database
$db = $rcmail->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error())
die("No DB connection\n");
- $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','distribution-list','event','task','configuration');
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','distribution-list','event','task','configuration','file');
$folder_types_db = array_map(array($db, 'quote'), $folder_types);
if ($opts['all']) {
$sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ")";
}
else if ($opts['username']) {
$sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
}
if ($sql_query) {
$db->query($sql_query . $sql_add, resource_prefix($opts).'%');
echo $db->affected_rows() . " records deleted from 'kolab_cache'\n";
}
break;
/*
* Prewarm cache by synchronizing objects for the given user
*/
case 'prewarm':
// make sure libkolab classes are loaded
$rcmail->plugins->load_plugin('libkolab');
if (authenticate($opts)) {
- $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','event','task','configuration');
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','event','task','configuration','file');
foreach ($folder_types as $type) {
// sync every folder of the given type
foreach (kolab_storage::get_folders($type) as $folder) {
echo "Synching " . $folder->name . " ($type) ... ";
echo $folder->count($type) . "\n";
// also sync distribution lists in contact folders
if ($type == 'contact') {
echo "Synching " . $folder->name . " (distribution-list) ... ";
echo $folder->count('distribution-list') . "\n";
}
}
}
}
else
die("Authentication failed for " . $opts['user']);
break;
/*
* Unknown action => show usage
*/
default:
print_usage();
exit;
}
/**
* Compose cache resource URI prefix for the given user credentials
*/
function resource_prefix($opts)
{
return 'imap://' . urlencode($opts['username']) . '@' . $opts['host'] . '/';
}
/**
* Authenticate to the IMAP server with the given user credentials
*/
function authenticate(&$opts)
{
global $rcmail;
// prompt for password
if (empty($opts['password']) && ($opts['username'] || $opts['user'])) {
$opts['password'] = prompt_silent("Password: ");
}
// simulate "login as" feature
if ($opts['user'] && $opts['user'] != $opts['username'])
$_POST['_loginas'] = $opts['username'];
else if (empty($opts['user']))
$opts['user'] = $opts['username'];
// let the kolab_auth plugin do its magic
$auth = $rcmail->plugins->exec_hook('authenticate', array(
'host' => trim($opts['host']),
'user' => trim($opts['user']),
'pass' => $opts['password'],
'cookiecheck' => false,
'valid' => !empty($opts['user']) && !empty($opts['host']),
));
if ($auth['valid']) {
$storage = $rcmail->get_storage();
if ($storage->connect($auth['host'], $auth['user'], $auth['pass'], 143, false)) {
if ($opts['verbose'])
echo "IMAP login succeeded.\n";
if (($user = rcube_user::query($opts['username'], $auth['host'])) && $user->ID)
$rcmail->set_user($user);
}
else
die("Login to IMAP server failed!\n");
}
else {
die("Invalid login credentials!\n");
}
return $auth['valid'];
}
diff --git a/lib/plugins/libkolab/config.inc.php.dist b/lib/plugins/libkolab/config.inc.php.dist
index 01e1334..aa0c8d0 100644
--- a/lib/plugins/libkolab/config.inc.php.dist
+++ b/lib/plugins/libkolab/config.inc.php.dist
@@ -1,22 +1,26 @@
<?php
/* Configuration for libkolab */
// Enable caching of Kolab objects in local database
$rcmail_config['kolab_cache'] = true;
// Specify format version to write Kolab objects (must be a string value!)
$rcmail_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
$rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
// Set this option to disable SSL certificate checks when triggering Free/Busy (enabled by default)
$rcmail_config['kolab_ssl_verify_peer'] = false;
// Enables listing of only subscribed folders. This e.g. will limit
// folders in calendar view or available addressbooks
$rcmail_config['kolab_use_subscriptions'] = false;
+// Enables the use of displayname folder annotations as introduced in KEP:?
+// for displaying resource folder names (experimental!)
+$rcmail_config['kolab_custom_display_names'] = false;
+
?>
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 0c571f2..72867fc 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -1,450 +1,450 @@
<?php
/**
* Kolab Contact 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_contact extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.contact';
protected $objclass = 'Contact';
protected $read_func = 'readContact';
protected $write_func = 'writeContact';
public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email:address');
public $phonetypes = array(
'home' => Telephone::Home,
'work' => Telephone::Work,
'text' => Telephone::Text,
'main' => Telephone::Voice,
'homefax' => Telephone::Fax,
'workfax' => Telephone::Fax,
'mobile' => Telephone::Cell,
'video' => Telephone::Video,
'pager' => Telephone::Pager,
'car' => Telephone::Car,
'other' => Telephone::Textphone,
);
public $emailtypes = array(
'home' => Email::Home,
'work' => Email::Work,
'other' => Email::NoType,
);
public $addresstypes = array(
'home' => Address::Home,
'work' => Address::Work,
'office' => 0,
);
private $gendermap = array(
'female' => Contact::Female,
'male' => Contact::Male,
);
private $relatedmap = array(
'manager' => Related::Manager,
'assistant' => Related::Assistant,
'spouse' => Related::Spouse,
'children' => Related::Child,
);
/**
* Default constructor
*/
function __construct($xmldata = null, $version = 3.0)
{
parent::__construct($xmldata, $version);
// complete phone types
$this->phonetypes['homefax'] |= Telephone::Home;
$this->phonetypes['workfax'] |= Telephone::Work;
}
/**
* Set contact properties to the kolabformat object
*
* @param array Contact data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
// do the hard work of setting object values
$nc = new NameComponents;
$nc->setSurnames(self::array2vector($object['surname']));
$nc->setGiven(self::array2vector($object['firstname']));
$nc->setAdditional(self::array2vector($object['middlename']));
$nc->setPrefixes(self::array2vector($object['prefix']));
$nc->setSuffixes(self::array2vector($object['suffix']));
$this->obj->setNameComponents($nc);
$this->obj->setName($object['name']);
$this->obj->setCategories(self::array2vector($object['categories']));
if (isset($object['nickname']))
$this->obj->setNickNames(self::array2vector($object['nickname']));
if (isset($object['profession']))
$this->obj->setTitles(self::array2vector($object['profession']));
// organisation related properties (affiliation)
$org = new Affiliation;
$offices = new vectoraddress;
if ($object['organization'])
$org->setOrganisation($object['organization']);
if ($object['department'])
$org->setOrganisationalUnits(self::array2vector($object['department']));
if ($object['jobtitle'])
$org->setRoles(self::array2vector($object['jobtitle']));
$rels = new vectorrelated;
if ($object['manager']) {
foreach ((array)$object['manager'] as $manager)
$rels->push(new Related(Related::Text, $manager, Related::Manager));
}
if ($object['assistant']) {
foreach ((array)$object['assistant'] as $assistant)
$rels->push(new Related(Related::Text, $assistant, Related::Assistant));
}
$org->setRelateds($rels);
// im, email, url
$this->obj->setIMaddresses(self::array2vector($object['im']));
if (class_exists('vectoremail')) {
$vemails = new vectoremail;
foreach ((array)$object['email'] as $email) {
$type = $this->emailtypes[$email['type']];
$vemails->push(new Email($email['address'], intval($type)));
}
}
else {
$vemails = self::array2vector(array_map(function($v){ return $v['address']; }, $object['email']));
}
$this->obj->setEmailAddresses($vemails);
$vurls = new vectorurl;
foreach ((array)$object['website'] as $url) {
$type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
$vurls->push(new Url($url['url'], $type));
}
$this->obj->setUrls($vurls);
// addresses
$adrs = new vectoraddress;
foreach ((array)$object['address'] as $address) {
$adr = new Address;
$type = $this->addresstypes[$address['type']];
if (isset($type))
$adr->setTypes($type);
else if ($address['type'])
$adr->setLabel($address['type']);
if ($address['street'])
$adr->setStreet($address['street']);
if ($address['locality'])
$adr->setLocality($address['locality']);
if ($address['code'])
$adr->setCode($address['code']);
if ($address['region'])
$adr->setRegion($address['region']);
if ($address['country'])
$adr->setCountry($address['country']);
if ($address['type'] == 'office')
$offices->push($adr);
else
$adrs->push($adr);
}
$this->obj->setAddresses($adrs);
$org->setAddresses($offices);
// add org affiliation after addresses are set
$orgs = new vectoraffiliation;
$orgs->push($org);
$this->obj->setAffiliations($orgs);
// telephones
$tels = new vectortelephone;
foreach ((array)$object['phone'] as $phone) {
$tel = new Telephone;
if (isset($this->phonetypes[$phone['type']]))
$tel->setTypes($this->phonetypes[$phone['type']]);
$tel->setNumber($phone['number']);
$tels->push($tel);
}
$this->obj->setTelephones($tels);
if (isset($object['gender']))
$this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
if (isset($object['notes']))
$this->obj->setNote($object['notes']);
if (isset($object['freebusyurl']))
$this->obj->setFreeBusyUrl($object['freebusyurl']);
if (isset($object['birthday']))
$this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
if (isset($object['anniversary']))
$this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
if (!empty($object['photo'])) {
if ($type = rcube_mime::image_content_type($object['photo']))
$this->obj->setPhoto($object['photo'], $type);
}
else if (isset($object['photo']))
$this->obj->setPhoto('','');
else if ($this->obj->photoMimetype()) // load saved photo for caching
$object['photo'] = $this->obj->photo();
// spouse and children are relateds
$rels = new vectorrelated;
if ($object['spouse']) {
$rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
}
if ($object['children']) {
foreach ((array)$object['children'] as $child)
$rels->push(new Related(Related::Text, $child, Related::Child));
}
$this->obj->setRelateds($rels);
// insert/replace crypto keys
$pgp_index = $pkcs7_index = -1;
$keys = $this->obj->keys();
for ($i=0; $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($pgp_index < 0 && $key->type() == Key::PGP)
$pgp_index = $i;
else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
$pkcs7_index = $i;
}
$pgpkey = $object['pgppublickey'] ? new Key($object['pgppublickey'], Key::PGP) : new Key();
$pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
if ($pgp_index >= 0)
$keys->set($pgp_index, $pgpkey);
else if (!empty($object['pgppublickey']))
$keys->push($pgpkey);
if ($pkcs7_index >= 0)
$keys->set($pkcs7_index, $pkcs7key);
else if (!empty($object['pkcs7publickey']))
$keys->push($pkcs7key);
$this->obj->setKeys($keys);
// TODO: handle language, gpslocation, etc.
// set type property for proper caching
$object['_type'] = 'contact';
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
}
/**
* Convert the Contact object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Contact 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);
$object['name'] = $this->obj->name();
$nc = $this->obj->nameComponents();
$object['surname'] = join(' ', self::vector2array($nc->surnames()));
$object['firstname'] = join(' ', self::vector2array($nc->given()));
$object['middlename'] = join(' ', self::vector2array($nc->additional()));
$object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
$object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
$object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
$object['profession'] = join(' ', self::vector2array($this->obj->titles()));
$object['categories'] = self::vector2array($this->obj->categories());
// organisation related properties (affiliation)
$orgs = $this->obj->affiliations();
if ($orgs->size()) {
$org = $orgs->get(0);
$object['organization'] = $org->organisation();
$object['jobtitle'] = join(' ', self::vector2array($org->roles()));
$object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
$this->read_relateds($org->relateds(), $object);
}
$object['im'] = self::vector2array($this->obj->imAddresses());
$emails = $this->obj->emailAddresses();
if ($emails instanceof vectoremail) {
$emailtypes = array_flip($this->emailtypes);
for ($i=0; $i < $emails->size(); $i++) {
$email = $emails->get($i);
$object['email'][] = array('address' => $email->address(), 'type' => $emailtypes[$email->types()]);
}
}
else {
$object['email'] = self::vector2array($emails);
}
$urls = $this->obj->urls();
for ($i=0; $i < $urls->size(); $i++) {
$url = $urls->get($i);
$subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
$object['website'][] = array('url' => $url->url(), 'type' => $subtype);
}
// addresses
$this->read_addresses($this->obj->addresses(), $object);
if ($org && ($offices = $org->addresses()))
$this->read_addresses($offices, $object, 'office');
// telehones
$tels = $this->obj->telephones();
$teltypes = array_flip($this->phonetypes);
for ($i=0; $i < $tels->size(); $i++) {
$tel = $tels->get($i);
$object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
}
$object['notes'] = $this->obj->note();
$object['freebusyurl'] = $this->obj->freeBusyUrl();
if ($bday = self::php_datetime($this->obj->bDay()))
$object['birthday'] = $bday->format('c');
if ($anniversary = self::php_datetime($this->obj->anniversary()))
$object['anniversary'] = $anniversary->format('c');
$gendermap = array_flip($this->gendermap);
if (($g = $this->obj->gender()) && $gendermap[$g])
$object['gender'] = $gendermap[$g];
if ($this->obj->photoMimetype())
$object['photo'] = $this->obj->photo();
else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName()))
$object['photo'] = $photo_name;
// relateds -> spouse, children
$this->read_relateds($this->obj->relateds(), $object);
// crypto settings: currently only key values are supported
$keys = $this->obj->keys();
for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($key->type() == Key::PGP)
$object['pgppublickey'] = $key->key();
else if ($key->type() == Key::PKCS7_MIME)
$object['pkcs7publickey'] = $key->key();
}
$this->data = $object;
return $this->data;
}
/**
* 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 $col) {
+ 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));
}
/**
* Helper method to copy contents of an Address vector to the contact data object
*/
private function read_addresses($addresses, &$object, $type = null)
{
$adrtypes = array_flip($this->addresstypes);
for ($i=0; $i < $addresses->size(); $i++) {
$adr = $addresses->get($i);
$object['address'][] = array(
'type' => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
'street' => $adr->street(),
'code' => $adr->code(),
'locality' => $adr->locality(),
'region' => $adr->region(),
'country' => $adr->country()
);
}
}
/**
* Helper method to map contents of a Related vector to the contact data object
*/
private function read_relateds($rels, &$object)
{
$typemap = array_flip($this->relatedmap);
for ($i=0; $i < $rels->size(); $i++) {
$rel = $rels->get($i);
if ($rel->type() != Related::Text) // we can't handle UID relations yet
continue;
$types = $rel->relationTypes();
foreach ($typemap as $t => $field) {
if ($types & $t) {
$object[$field][] = $rel->text();
break;
}
}
}
}
}
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
index e050774..f3d0470 100644
--- a/lib/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -1,262 +1,222 @@
<?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';
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;
$this->obj->setStatus($status);
- // 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']);
- $vattach->push($attach);
- }
-
- foreach ((array)$object['links'] as $link) {
- $attach = new Attachment;
- $attach->setUri($link, null);
- $vattach->push($attach);
- }
-
- $this->obj->setAttachments($vattach);
-
// save recurrence exceptions
if ($object['recurrence']['EXCEPTIONS']) {
$vexceptions = new vectorevent;
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$exevent = new kolab_format_event;
$exevent->set($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);
}
$this->obj->setExceptions($vexceptions);
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return $this->data || (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(),
);
// 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;
- // 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();
- $content = $attach->data();
- $object['_attachments'][$name] = array(
- 'name' => $name,
- 'mimetype' => $attach->mimetype(),
- 'size' => strlen($content),
- 'content' => $content,
- );
- }
- else if (substr($attach->uri(), 0, 4) == 'http') {
- $object['links'][] = $attach->uri();
- }
- }
-
// read exception event objects
if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
for ($i=0; $i < $exceptions->size(); $i++) {
if (($exobj = $exceptions->get($i))) {
$exception = new kolab_format_event($exobj);
if ($exception->is_valid()) {
$object['recurrence']['EXCEPTIONS'][] = $this->expand_exception($exception->to_array(), $object);
}
}
}
}
// this is an exception object
else if ($this->obj->recurrenceID()->isValid()) {
$object['thisandfuture'] = $this->obj->thisAndFuture();
}
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 = array();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
if (!empty($this->data['alarms'])) {
$tags[] = 'x-has-alarms';
}
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]);
}
}
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/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index ff10a10..085e577 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -1,411 +1,452 @@
<?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');
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,
);
private $status_map = array(
'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
'CANCELLED' => kolabformat::StatusCancelled,
);
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()),
);
// 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']) {
$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(),
);
}
}
// 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;
}
}
}
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
for ($i=0; $i < $valarms->size(); $i++) {
$alarm = $valarms->get($i);
$type = $alarm_types[$alarm->type()];
if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported
if ($start = self::php_datetime($alarm->start())) {
$object['alarms'] = '@' . $start->format('U');
}
else if ($offset = $alarm->relativeStart()) {
$value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
if ($w = $offset->weeks()) $value .= $w . 'W';
else if ($d = $offset->days()) $value .= $d . 'D';
else if ($h = $offset->hours()) $value .= $h . 'H';
else if ($m = $offset->minutes()) $value .= $m . 'M';
else if ($s = $offset->seconds()) $value .= $s . 'S';
else continue;
$object['alarms'] = $value;
}
$object['alarms'] .= ':' . $type;
break;
}
}
+ // 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();
+ $content = $attach->data();
+ $object['_attachments'][$name] = array(
+ 'name' => $name,
+ 'mimetype' => $attach->mimetype(),
+ 'size' => strlen($content),
+ 'content' => $content,
+ );
+ }
+ else if (substr($attach->uri(), 0, 4) == 'http') {
+ $object['links'][] = $attach->uri();
+ }
+ }
+
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();
// set common object properties
parent::set($object);
// increment sequence on updates
- $object['sequence'] = !$is_new ? $this->obj->sequence()+1 : 0;
+ if (empty($object['sequence']))
+ $object['sequence'] = !$is_new ? $this->obj->sequence()+1 : 0;
$this->obj->setSequence($object['sequence']);
$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']));
// process event attendees
$attendees = new vectorattendee;
foreach ((array)$object['attendees'] as $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']);
$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']);
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']) {
$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 alarm
$valarms = new vectoralarm;
if ($object['alarms']) {
list($offset, $type) = explode(":", $object['alarms']);
if ($type == 'EMAIL') { // 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('/^([-+]?)(\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);
+
+ // 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']);
+ $vattach->push($attach);
+ }
+
+ foreach ((array)$object['links'] as $link) {
+ $attach = new Attachment;
+ $attach->setUri($link, 'unknown');
+ $vattach->push($attach);
+ }
+
+ $this->obj->setAttachments($vattach);
}
/**
* 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));
}
}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index 2f3cdc5..ee6ede0 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -1,884 +1,1019 @@
<?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, 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';
public static $version = '3.0';
public static $last_error;
private static $ready = false;
private static $subscriptions;
private static $states;
private static $config;
private static $imap;
+ // 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,
));
self::$imap->set_pagesize(9999);
}
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;
}
/**
* 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)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $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 null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder)
{
return self::setup() ? new kolab_storage_folder($folder) : 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,distribution-list,event,task,note)
* @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) {
if (!$folder)
$folder = new kolab_storage_folder($foldername);
else
$folder->set_folder($foldername);
if ($object = $folder->get_object($uid))
return $object;
}
return false;
}
/**
*
*/
public static function get_freebusy_server()
{
return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
}
/**
* 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)
*
* @return string Folder ID string
*/
public static function folder_id($folder)
{
return asciiwords(strtr($folder, '/.-', '___'));
}
/**
* 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();
$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();
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();
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
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']);
}
- // save displayname and color in METADATA
- // TODO: also save 'showalarams' and other properties here
if ($result) {
- $ns = null;
- foreach (array('color' => array(self::COLOR_KEY_SHARED,self::COLOR_KEY_PRIVATE),
- 'displayname' => array(self::NAME_KEY_SHARED,self::NAME_KEY_PRIVATE)) as $key => $metakeys) {
- if (!empty($prop[$key])) {
- if (!isset($ns))
- $ns = self::$imap->folder_namespace($folder);
-
- $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
- }
- }
+ 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
- $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;
+ 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;
+ }
}
$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), ' » ', $folder);
if ($prefix)
$folder = html::quote($prefix) . ' ' . $folder;
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Helper method to generate a truncated folder name to display
*/
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] . ' » ') === 0) {
$length = strlen($names[$i] . ' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count-1) . '» ' . 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
$folders = self::get_folders($type);
$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 && ($name == $current || 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);
}
// 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);
}
// Sort folders list
asort($names, SORT_LOCALE_STRING);
// 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].' » ') === 0) {
$length = strlen($listnames[$i].' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count-1) . '» ' . 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,distribution-list,event,task,note,mail)
* @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) {
return self::$imap->list_folders_subscribed($root, $mbox);
}
else {
return self::$imap->list_folders($root, $mbox);
}
}
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// get folders types
$folderdata = self::folders_typedata($prefix);
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 array_keys($folderdata);
}
// Get folders list
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
}
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) {
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
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)
+ {
+ $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)); // decode »
+ }
+
+ $names = array();
+ foreach ($nsnames as $ns => $dummy) {
+ asort($nsnames[$ns], SORT_LOCALE_STRING);
+ $names += $nsnames[$ns];
+ }
+
+ $out = array();
+ foreach ($names as $utf7name => $name) {
+ $out[] = $folders[$utf7name];
+ }
+
+ return $out;
+ }
+
+
/**
* 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
*/
- static function folders_typedata($prefix = '*')
+ public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
$folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($folderdata)) {
return false;
}
return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
}
/**
* Callback for array_map to select the correct annotation value
*/
- static function folder_select_metadata($types)
+ 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]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
- static function folder_type($folder)
+ public static function folder_type($folder)
{
self::setup();
$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
*/
- static function set_folder_type($folder, $type='mail')
+ 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
*
* @return boolean True if subscribed, false if not
*/
public static function folder_is_subscribed($folder)
{
if (self::$subscriptions === null) {
self::setup();
self::$subscriptions = self::$imap->list_folders_subscribed();
}
return in_array($folder, self::$subscriptions);
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_subscribe($folder)
{
self::setup();
if (self::$imap->subscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_unsubscribe($folder)
{
self::setup();
if (self::$imap->unsubscribe($folder)) {
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)
{
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)
{
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::$subscriptions = self::$imap->list_folders_subscribed();
}
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 ($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->folder_create($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
+ }
+ }
+ }
+
}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index a3b6626..ba6c106 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -1,768 +1,768 @@
<?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, 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
{
private $db;
private $imap;
private $folder;
private $uid2msg;
private $objects;
private $index = array();
private $resource_uri;
private $enabled = true;
private $synched = false;
private $synclock = false;
private $ready = false;
private $max_sql_packet;
private $max_sync_lock_time = 600;
private $binary_items = array(
'photo' => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
'pgppublickey' => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></photo>|i',
'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></photo>|i',
);
/**
* 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);
if ($this->enabled) {
// remove sync-lock on script termination
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
}
if ($storage_folder)
$this->set_folder($storage_folder);
}
/**
* 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)) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
$this->ready = $this->enabled;
}
/**
* 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);
// lock synchronization for this folder or wait if locked
$this->_sync_lock();
// 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);
$this->index = $imap_index->get();
// determine objects to fetch or to invalidate
if ($this->ready) {
// read cache index
$sql_result = $this->db->query(
"SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
$this->resource_uri,
'lock'
);
$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
foreach (array_diff($this->index, $old_index) as $msguid) {
if ($object = $this->folder->read_object($msguid, '*')) {
$this->_extended_insert($msguid, $object);
}
}
$this->_extended_insert(0, null);
// delete invalid entries from local DB
$del_index = array_diff($old_index, $this->index);
if (!empty($del_index)) {
$quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
$this->db->query(
"DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
$this->resource_uri
);
}
}
// remove lock
$this->_sync_unlock();
$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);
}
// load object if not in memory
if (!isset($this->objects[$msguid])) {
if ($this->ready) {
$sql_result = $this->db->query(
"SELECT * FROM kolab_cache ".
"WHERE resource=? AND type=? AND msguid=?",
$this->resource_uri,
$type ?: $this->folder->type,
$msguid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->objects[$msguid] = $this->_unserialize($sql_arr);
}
}
// fetch from IMAP if not present in cache
if (empty($this->objects[$msguid])) {
$result = $this->_fetch(array($msguid), $type, $foldername);
$this->objects[$msguid] = $result[0];
}
}
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;
}
// remove old entry
if ($this->ready) {
$this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=? AND type<>?",
$this->resource_uri, $msguid, 'lock');
}
if ($object) {
// insert new object data...
$this->insert($msguid, $object);
}
else {
// ...or set in-memory cache to false
$this->objects[$msguid] = $object;
}
}
/**
* Insert 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
*/
public function insert($msguid, $object)
{
// write to cache
if ($this->ready) {
$sql_data = $this->_serialize($object);
$objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
$result = $this->db->query(
"INSERT INTO kolab_cache ".
" (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words, filename)".
" VALUES (?, ?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ?, ?, ?, ?)",
$this->resource_uri,
$objtype,
$msguid,
$object['uid'],
$sql_data['changed'],
$sql_data['data'],
$sql_data['xml'],
$sql_data['dtstart'],
$sql_data['dtend'],
$sql_data['tags'],
$sql_data['words'],
$sql_data['filename']
);
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[$msguid] = $object;
$this->uid2msg[$object['uid']] = $msguid;
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
* @param string Target IMAP folder to move it to
*/
public function move($msguid, $uid, $target_folder)
{
$target = kolab_storage::get_folder($target_folder);
// resolve new message UID in target folder
if ($new_msguid = $target->cache->uid2msguid($uid)) {
$this->db->query(
"UPDATE kolab_cache SET resource=?, msguid=? ".
"WHERE resource=? AND msguid=? AND type<>?",
$target->get_resource_uri(),
$new_msguid,
$this->resource_uri,
$msguid,
'lock'
);
}
else {
// just clear cache entry
$this->set($msguid, false);
}
unset($this->uid2msg[$uid]);
}
/**
* Remove all objects from local cache
*/
public function purge($type = null)
{
$result = $this->db->query(
"DELETE FROM kolab_cache WHERE resource=?".
($type ? ' AND type=?' : ''),
$this->resource_uri,
$type
);
return $this->db->affected_rows($result);
}
/**
* 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 = array();
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$sql_result = $this->db->query(
"SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM kolab_cache ".
"WHERE resource=? " . $this->_sql_where($query),
$this->resource_uri
);
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 ($object = $this->_unserialize($sql_arr)) {
$result[] = $object;
}
}
}
else {
// extract object type from query parameter
$filter = $this->_query2assoc($query);
// use 'list' for folder's default objects
if ($filter['type'] == $this->type) {
$index = $this->index;
}
else { // search by object type
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
$index = $this->imap->search_once($this->folder->name, $search)->get();
}
// fetch all messages in $index from IMAP
$result = $uids ? $this->_fetch_uids($index, $filter['type']) : $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[$msguid] = $result[0];
}
}
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())
{
$count = 0;
// cache is in sync, we can count records in local DB
if ($this->synched) {
$sql_result = $this->db->query(
"SELECT COUNT(*) AS numrows FROM kolab_cache ".
"WHERE resource=? " . $this->_sql_where($query),
$this->resource_uri
);
$sql_arr = $this->db->fetch_assoc($sql_result);
$count = intval($sql_arr['numrows']);
}
else {
// search IMAP by object type
$filter = $this->_query2assoc($query);
$ctype = kolab_format::KTYPE_PREFIX . $filter['type'];
$index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
$count = $index->count();
}
return $count;
}
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
*/
private function _sql_where($query)
{
$sql_where = '';
foreach ($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] = '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
*/
private 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
*/
private function _fetch($index, $type = null, $folder = null)
{
$results = array();
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
$this->set($msguid, $object);
}
}
return $results;
}
/**
* Fetch object UIDs (aka message subjects) 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
*/
private function _fetch_uids($index, $type = null)
{
if (!$type)
$type = $this->folder->type;
$results = array();
foreach ((array)$this->imap->fetch_headers($this->folder->name, $index, false) as $msguid => $headers) {
$object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
// check object type header and abort on mismatch
if ($type != '*' && $object_type != $type)
return false;
$uid = $headers->subject;
$this->uid2msg[$uid] = $msguid;
$results[] = $uid;
}
return $results;
}
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*/
private function _serialize($object)
{
$sql_data = array('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
$objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
// set type specific values
if ($objtype == 'event') {
// database runs in server's timezone so using date() is what we want
$sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
$sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']);
// extend date range for recurring events
if ($object['recurrence'] && $object['_formatobj']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
$sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year'));
}
}
else if ($objtype == 'task') {
if ($object['start'])
$sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
if ($object['due'])
$sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['due']) ? $object['due']->format('U') : $object['due']);
}
else if ($objtype == 'file') {
if (!empty($object['_attachments'])) {
reset($object['_attachments']);
$sql_data['filename'] = $object['_attachments'][key($object['_attachments'])]['name'];
}
}
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;
}
}
}
$sql_data['data'] = serialize($data);
return $sql_data;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
private function _unserialize($sql_arr)
{
$object = unserialize($sql_arr['data']);
// 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]);
}
}
// add meta data
$object['_type'] = $sql_arr['type'];
$object['_msguid'] = $sql_arr['msguid'];
$object['_mailbox'] = $this->folder->name;
$object['_size'] = strlen($sql_arr['xml']);
$object['_formatobj'] = kolab_format::factory($sql_arr['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
*/
private function _extended_insert($msguid, $object)
{
static $buffer = '';
$line = '';
if ($object) {
$sql_data = $this->_serialize($object);
$objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
$values = array(
$this->db->quote($this->resource_uri),
$this->db->quote($objtype),
$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['dtstart']),
$this->db->quote($sql_data['dtend']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
$this->db->quote($sql_data['filename']),
);
$line = '(' . join(',', $values) . ')';
}
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
$result = $this->db->query(
"INSERT INTO kolab_cache ".
" (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words, filename)".
" 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
*/
private 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;
}
/**
* Check lock record for this folder and wait if locked or set lock
*/
private function _sync_lock()
{
if (!$this->ready)
return;
$sql_query = "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
"WHERE resource=? AND type=?";
$sql_arr = $this->db->fetch_assoc($this->db->query($sql_query, $this->resource_uri, 'lock'));
// abort if database is not set-up
if ($this->db->is_error()) {
$this->ready = false;
return;
}
$this->synclock = true;
// wait if locked (expire locks after 10 minutes)
while ($sql_arr && intval($sql_arr['locked']) > 0 && $sql_arr['created'] + $this->max_sync_lock_time > time()) {
usleep(500000);
$sql_arr = $this->db->fetch_assoc($this->db->query($sql_query, $this->resource_uri, 'lock'));
}
// create lock record if not exists
if (!$sql_arr) {
$this->db->query(
- "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml)".
- " VALUES (?, ?, 1, " . $this->db->now() . ", '', '', '')",
+ "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml, tags, words)".
+ " VALUES (?, ?, 1, " . $this->db->now() . ", '', '', '', '', '')",
$this->resource_uri,
'lock'
);
}
else {
$this->db->query(
"UPDATE kolab_cache SET msguid = 1, created = " . $this->db->now() .
" WHERE resource = ? AND type = ?",
$this->resource_uri,
'lock'
);
}
}
/**
* Remove lock for this folder
*/
public function _sync_unlock()
{
if (!$this->ready || !$this->synclock)
return;
$this->db->query(
"UPDATE kolab_cache SET msguid = 0 WHERE resource = ? AND type = ?",
$this->resource_uri,
'lock'
);
$this->synclock = false;
}
/**
* 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)
{
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] = $results[0];
}
return $this->uid2msg[$uid];
}
}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index f046bbf..303ed99 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -1,1126 +1,1130 @@
<?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, 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
{
/**
* The folder name.
* @var string
*/
public $name;
/**
* The type of this folder.
* @var string
*/
public $type;
/**
* Is this folder set to be the default for its type
* @var boolean
*/
public $default = false;
/**
* Is this folder set to be default
* @var boolean
*/
public $cache;
private $type_annotation;
private $namespace;
private $imap;
private $info;
private $idata;
private $owner;
private $resource_uri;
/**
* Default constructor
*/
function __construct($name, $type = null)
{
$this->imap = rcube::get_instance()->get_storage();
$this->imap->set_options(array('skip_deleted' => true));
$this->cache = new kolab_storage_cache($this);
$this->set_folder($name, $type);
}
/**
* Set the IMAP folder this instance connects to
*
* @param string The folder name/path
* @param string Optional folder type if known
*/
public function set_folder($name, $ftype = null)
{
$this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
list($this->type, $suffix) = explode('.', $this->type_annotation);
$this->default = $suffix == 'default';
$this->name = $name;
$this->resource_uri = null;
$this->imap->set_folder($this->name);
$this->cache->set_folder($this);
}
/**
*
*/
private 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;
}
/**
* 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 = $this->imap->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);
}
/**
* Returns the owner of the folder.
*
* @return string The owner of this folder.
*/
public function get_owner()
{
// 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;
break;
}
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 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);
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return kolab_storage::object_name($this->name, $this->namespace);
}
/**
* 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;
}
/**
* 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;
return $this->resource_uri;
}
/**
* 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($type_or_query = null)
{
if (!$type_or_query)
$query = array(array('type','=',$this->type));
else if (is_string($type_or_query))
$query = array(array('type','=',$type_or_query));
else
$query = $this->_prepare_query((array)$type_or_query);
// synchronize cache first
$this->cache->synchronize();
return $this->cache->count($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;
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select(array(array('type','=',$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())
{
// check query argument
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())
{
// synchronize caches
$this->cache->synchronize();
// fetch UIDs from cache
return $this->cache->select($this->_prepare_query($query), true);
}
/**
* Helper method to sanitize query arguments
*/
private function _prepare_query($query)
{
$type = null;
foreach ($query as $i => $param) {
if ($param[0] == 'type') {
$type = $param[2];
}
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]);
}
}
// add type selector if not in $query
if (!$type)
$query[] = array('type','=',$this->type);
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)
{
// 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))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
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 (!$type) $type = $this->type;
if (!$folder) $folder = $this->name;
$this->imap->set_folder($folder);
$headers = $this->imap->get_message_headers($msguid);
$message = null;
// Message doesn't exist?
if (empty($headers)) {
return false;
}
// extract the X-Kolab-Type header from the XML attachment part if missing
if (empty($headers->others['x-kolab-type'])) {
$message = new rcube_message($msguid);
foreach ((array)$message->attachments as $part) {
if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
$headers->others['x-kolab-type'] = $part->mimetype;
break;
}
}
}
// fix buggy messages stating the X-Kolab-Type header twice
else if (is_array($headers->others['x-kolab-type'])) {
$headers->others['x-kolab-type'] = reset($headers->others['x-kolab-type']);
}
// no object type header found: abort
if (empty($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($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;
if (!$message) $message = new rcube_message($msguid);
$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);
}
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 = $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 (!$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'];
+
// 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);
// delete old message
if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
$this->imap->delete_message($object['_msguid'], $object['_mailbox']);
$this->cache->set($object['_msguid'], false, $object['_mailbox']);
}
// update cache with new UID
if ($result) {
$object['_msguid'] = $result;
$this->cache->insert($result, $object);
// 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'] = new DateTime('@'.$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)
{
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
if ($msguid && $expunge) {
$success = $this->imap->delete_message($msguid, $this->name);
}
else if ($msguid) {
$success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
}
if ($success) {
$this->cache->set($msguid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
$this->cache->purge();
return $this->imap->clear_folder($this->name);
}
/**
* Restore a previously deleted object
*
* @param string Object UID
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if ($msguid = $this->cache->uid2msguid($uid, true)) {
if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
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 ($msguid = $this->cache->uid2msguid($uid)) {
if ($this->imap->move_message($msguid, $target_folder, $this->name)) {
$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) {
$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 {
$rcmail = rcube::get_instance();
$request = new HTTP_Request2($url);
$request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
// 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;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 11, 12:19 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
421939
Default Alt Text
(222 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment