summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksander Machniak <machniak@kolabsys.com>2014-10-17 12:59:27 (GMT)
committerAleksander Machniak <machniak@kolabsys.com>2014-10-17 13:00:26 (GMT)
commit353d36956b56914accb78bd0f00fabc1ab05d38f (patch)
tree1f812d4fd62e539472efbfada322270b905743f6
parentf3332f9ffb376c7eec51f6b4a9819f5e5d8199b1 (diff)
downloadroundcubemail-plugins-kolab-353d36956b56914accb78bd0f00fabc1ab05d38f.tar.gz
Add support for configuration./file_driver objects (#3775)
Conflicts: plugins/libkolab/lib/kolab_format_configuration.php Added kolab_storage_config class
-rw-r--r--plugins/libkolab/lib/kolab_format.php2
-rw-r--r--plugins/libkolab/lib/kolab_format_configuration.php35
-rw-r--r--plugins/libkolab/lib/kolab_storage_cache_configuration.php48
-rw-r--r--plugins/libkolab/lib/kolab_storage_config.php840
4 files changed, 922 insertions, 3 deletions
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index ef6ba7a..fa85eec 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -174,7 +174,7 @@ abstract class kolab_format
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
- $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
+ $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
diff --git a/plugins/libkolab/lib/kolab_format_configuration.php b/plugins/libkolab/lib/kolab_format_configuration.php
index 174ab37..21dc587 100644
--- a/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/plugins/libkolab/lib/kolab_format_configuration.php
@@ -32,10 +32,12 @@ class kolab_format_configuration extends kolab_format
protected $write_func = 'writeConfiguration';
private $type_map = array(
- 'dictionary' => Configuration::TypeDictionary,
- 'category' => Configuration::TypeCategoryColor,
+ 'category' => Configuration::TypeCategoryColor,
+ 'dictionary' => Configuration::TypeDictionary,
+ 'file_driver' => Configuration::TypeFileDriver,
);
+ private $driver_settings_fields = array('host', 'port', 'username', 'password');
/**
* Set properties to the kolabformat object
@@ -60,6 +62,22 @@ class kolab_format_configuration extends kolab_format
$categories = new vectorcategorycolor;
$this->obj = new Configuration($categories);
break;
+
+ case 'file_driver':
+ $driver = new FileDriver($object['driver'], $object['title']);
+
+ $driver->setEnabled((bool) $object['enabled']);
+
+ foreach ($this->driver_settings_fields as $field) {
+ $value = $object[$field];
+ if ($value !== null) {
+ $driver->{'set' . ucfirst($field)}($value);
+ }
+ }
+
+ $this->obj = new Configuration($driver);
+ break;
+
default:
return false;
}
@@ -111,6 +129,19 @@ class kolab_format_configuration extends kolab_format
case 'category':
// TODO: implement this
break;
+
+ case 'file_driver':
+ $driver = $this->obj->file_driver();
+
+ $object['driver'] = $driver->driver();
+ $object['title'] = $driver->title();
+ $object['enabled'] = $driver->enabled();
+
+ foreach ($this->driver_settings_fields as $field) {
+ $object[$field] = $driver->{$field}();
+ }
+
+ break;
}
// adjust content-type string
diff --git a/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/plugins/libkolab/lib/kolab_storage_cache_configuration.php
index 8382852..c3c7ac4 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -37,4 +37,52 @@ class kolab_storage_cache_configuration extends kolab_storage_cache
return $sql_data;
}
+
+ /**
+ * Select Kolab objects filtered by the given query
+ *
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * @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)
+ {
+ // modify query for IMAP search: query param 'type' is actually a subtype
+ if (!$this->ready) {
+ foreach ($query as $i => $tuple) {
+ if ($tuple[0] == 'type') {
+ $tuple[2] = 'configuration.' . $tuple[2];
+ $query[$i] = $tuple;
+ }
+ }
+ }
+
+ return parent::select($query, $uids);
+ }
+
+ /**
+ * Helper method to compose a valid SQL query from pseudo filter triplets
+ */
+ protected function _sql_where($query)
+ {
+ if (is_array($query)) {
+ foreach ($query as $idx => $param) {
+ // convert category filter
+ if ($param[0] == 'category') {
+ $param[2] = array_map(function($n) { return 'category:' . $n; }, (array) $param[2]);
+
+ $query[$idx][0] = 'tags';
+ $query[$idx][2] = count($param[2]) > 1 ? $param[2] : $param[2][0];
+ }
+ // convert member filter (we support only = operator with single value)
+ else if ($param[0] == 'member') {
+ $query[$idx][0] = 'words';
+ $query[$idx][1] = '~';
+ $query[$idx][2] = '^' . $param[2] . '$';
+ }
+ }
+ }
+
+ return parent::_sql_where($query);
+ }
}
diff --git a/plugins/libkolab/lib/kolab_storage_config.php b/plugins/libkolab/lib/kolab_storage_config.php
new file mode 100644
index 0000000..d58e3c0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_config.php
@@ -0,0 +1,840 @@
+<?php
+
+/**
+ * Kolab storage class providing access to configuration objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_config
+{
+ const FOLDER_TYPE = 'configuration';
+
+
+ /**
+ * Singleton instace of kolab_storage_config
+ *
+ * @var kolab_storage_config
+ */
+ static protected $instance;
+
+ private $folders;
+ private $default;
+ private $enabled;
+
+
+ /**
+ * This implements the 'singleton' design pattern
+ *
+ * @return kolab_storage_config The one and only instance
+ */
+ static function get_instance()
+ {
+ if (!self::$instance) {
+ self::$instance = new kolab_storage_config();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Private constructor (finds default configuration folder as a config source)
+ */
+ private function __construct()
+ {
+ // get all configuration folders
+ $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
+
+ foreach ($this->folders as $folder) {
+ if ($folder->default) {
+ $this->default = $folder;
+ break;
+ }
+ }
+
+ // if no folder is set as default, choose the first one
+ if (!$this->default) {
+ $this->default = reset($this->folders);
+ }
+
+ // attempt to create a default folder if it does not exist
+ if (!$this->default) {
+ $folder_name = 'Configuration';
+ $folder_type = self::FOLDER_TYPE . '.default';
+
+ if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
+ $this->default = new kolab_storage_folder($folder_name, $folder_type);
+ }
+ }
+
+ // check if configuration folder exist
+ if ($this->default && $this->default->name) {
+ $this->enabled = true;
+ }
+ }
+
+ /**
+ * Check wether any configuration storage (folder) exists
+ *
+ * @return bool
+ */
+ public function is_enabled()
+ {
+ return $this->enabled;
+ }
+
+ /**
+ * Get configuration objects
+ *
+ * @param array $filter Search filter
+ * @param bool $default Enable to get objects only from default folder
+ * @param int $limit Max. number of records (per-folder)
+ *
+ * @return array List of objects
+ */
+ public function get_objects($filter = array(), $default = false, $limit = 0)
+ {
+ $list = array();
+
+ foreach ($this->folders as $folder) {
+ // we only want to read from default folder
+ if ($default && !$folder->default) {
+ continue;
+ }
+
+ // for better performance it's good to assume max. number of records
+ if ($limit) {
+ $folder->set_order_and_limit(null, $limit);
+ }
+
+ foreach ($folder->select($filter) as $object) {
+ unset($object['_formatobj']);
+ $list[] = $object;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Get configuration object
+ *
+ * @param string $uid Object UID
+ * @param bool $default Enable to get objects only from default folder
+ *
+ * @return array Object data
+ */
+ public function get_object($uid, $default = false)
+ {
+ foreach ($this->folders as $folder) {
+ // we only want to read from default folder
+ if ($default && !$folder->default) {
+ continue;
+ }
+
+ if ($object = $folder->get_object($uid)) {
+ return $object;
+ }
+ }
+ }
+
+ /**
+ * Create/update configuration object
+ *
+ * @param array $object Object data
+ * @param string $type Object type
+ *
+ * @return bool True on success, False on failure
+ */
+ public function save(&$object, $type)
+ {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ $folder = $this->find_folder($object);
+
+ if ($type) {
+ $object['type'] = $type;
+ }
+
+ return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
+ }
+
+ /**
+ * Remove configuration object
+ *
+ * @param string $uid Object UID
+ *
+ * @return bool True on success, False on failure
+ */
+ public function delete($uid)
+ {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ // fetch the object to find folder
+ $object = $this->get_object($uid);
+
+ if (!$object) {
+ return false;
+ }
+
+ $folder = $this->find_folder($object);
+
+ return $folder->delete($uid);
+ }
+
+ /**
+ * Find folder
+ */
+ public function find_folder($object = array())
+ {
+ // find folder object
+ if ($object['_mailbox']) {
+ foreach ($this->folders as $folder) {
+ if ($folder->name == $object['_mailbox']) {
+ break;
+ }
+ }
+ }
+ else {
+ $folder = $this->default;
+ }
+
+ return $folder;
+ }
+
+ /**
+ * Builds relation member URI
+ *
+ * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
+ *
+ * @return string $url Member URI
+ */
+ public static function build_member_url($params)
+ {
+ // param is object UUID
+ if (is_string($params) && !empty($params)) {
+ return 'urn:uuid:' . $params;
+ }
+
+ if (empty($params) || !strlen($params['folder'])) {
+ return null;
+ }
+
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+
+ // modify folder spec. according to namespace
+ $folder = $params['folder'];
+ $ns = $storage->folder_namespace($folder);
+
+ if ($ns == 'shared') {
+ // Note: this assumes there's only one shared namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = 'shared' . substr($folder, strlen($prefix));
+ }
+ }
+ }
+ else {
+ if ($ns == 'other') {
+ // Note: this assumes there's only one other users namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = 'user' . substr($folder, strlen($prefix));
+ }
+ }
+ }
+ else {
+ $folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
+ }
+ }
+
+ $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
+
+ // build URI
+ $url = 'imap:///' . $folder;
+
+ // UID is optional here because sometimes we want
+ // to build just a member uri prefix
+ if ($params['uid']) {
+ $url .= '/' . $params['uid'];
+ }
+
+ unset($params['folder']);
+ unset($params['uid']);
+
+ if (!empty($params)) {
+ $url .= '?' . http_build_query($params, '', '&');
+ }
+
+ return $url;
+ }
+
+ /**
+ * Parses relation member string
+ *
+ * @param string $url Member URI
+ *
+ * @return array Message folder, UID, Search headers (Message-Id, Date)
+ */
+ public static function parse_member_url($url)
+ {
+ // Look for IMAP URI:
+ // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
+ if (strpos($url, 'imap:///') === 0) {
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+
+ // parse_url does not work with imap:/// prefix
+ $url = parse_url(substr($url, 8));
+ $path = explode('/', $url['path']);
+ parse_str($url['query'], $params);
+
+ $uid = array_pop($path);
+ $ns = array_shift($path);
+ $path = array_map('rawurldecode', $path);
+
+ // resolve folder name
+ if ($ns == 'shared') {
+ $folder = implode('/', $path);
+ // Note: this assumes there's only one shared namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = $prefix . '/' . $folder;
+ }
+ }
+ }
+ else if ($ns == 'user') {
+ $username = array_shift($path);
+ $folder = implode('/', $path);
+
+ if ($username != $rcube->get_user_name()) {
+ // Note: this assumes there's only one other users namespace root
+ if ($ns = $storage->get_namespace('other')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = $prefix . '/' . $username . '/' . $folder;
+ }
+ }
+ }
+ else if (!strlen($folder)) {
+ $folder = 'INBOX';
+ }
+ }
+ else {
+ return;
+ }
+
+ return array(
+ 'folder' => $folder,
+ 'uid' => $uid,
+ 'params' => $params,
+ );
+ }
+
+ return false;
+ }
+
+ /**
+ * Build array of member URIs from set of messages
+ *
+ * @param string $folder Folder name
+ * @param array $messages Array of rcube_message objects
+ *
+ * @return array List of members (IMAP URIs)
+ */
+ public static function build_members($folder, $messages)
+ {
+ $members = array();
+
+ foreach ((array) $messages as $msg) {
+ $params = array(
+ 'folder' => $folder,
+ 'uid' => $msg->uid,
+ );
+
+ // add search parameters:
+ // we don't want to build "invalid" searches e.g. that
+ // will return false positives (more or wrong messages)
+ if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
+ $params['message-id'] = $messageid;
+ $params['date'] = $date;
+
+ if ($subject = $msg->get('subject', false)) {
+ $params['subject'] = substr($subject, 0, 256);
+ }
+ }
+
+ $members[] = self::build_member_url($params);
+ }
+
+ return $members;
+ }
+
+ /**
+ * Resolve/validate/update members (which are IMAP URIs) of relation object.
+ *
+ * @param array $tag Tag object
+ * @param bool $force Force members list update
+ *
+ * @return array Folder/UIDs list
+ */
+ public static function resolve_members(&$tag, $force = true)
+ {
+ $result = array();
+
+ foreach ((array) $tag['members'] as $member) {
+ // IMAP URI members
+ if ($url = self::parse_member_url($member)) {
+ $folder = $url['folder'];
+
+ if (!$force) {
+ $result[$folder][] = $url['uid'];
+ }
+ else {
+ $result[$folder]['uid'][] = $url['uid'];
+ $result[$folder]['params'][] = $url['params'];
+ $result[$folder]['member'][] = $member;
+ }
+ }
+ }
+
+ if (empty($result) || !$force) {
+ return $result;
+ }
+
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+ $search = array();
+ $missing = array();
+
+ // first we search messages by Folder+UID
+ foreach ($result as $folder => $data) {
+ // @FIXME: maybe better use index() which is cached?
+ // @TODO: consider skip_deleted option
+ $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
+ $uids = $index->get();
+
+ // messages that were not found need to be searched by search parameters
+ $not_found = array_diff($data['uid'], $uids);
+ if (!empty($not_found)) {
+ foreach ($not_found as $uid) {
+ $idx = array_search($uid, $data['uid']);
+
+ if ($p = $data['params'][$idx]) {
+ $search[] = $p;
+ }
+
+ $missing[] = $result[$folder]['member'][$idx];
+
+ unset($result[$folder]['uid'][$idx]);
+ unset($result[$folder]['params'][$idx]);
+ unset($result[$folder]['member'][$idx]);
+ }
+ }
+
+ $result[$folder] = $uids;
+ }
+
+ // search in all subscribed mail folders using search parameters
+ if (!empty($search)) {
+ // remove not found members from the members list
+ $tag['members'] = array_diff($tag['members'], $missing);
+
+ // get subscribed folders
+ $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
+
+ // @TODO: do this search in chunks (for e.g. 10 messages)?
+ $search_str = '';
+
+ foreach ($search as $p) {
+ $search_params = array();
+ foreach ($p as $key => $val) {
+ $key = strtoupper($key);
+ // don't search by subject, we don't want false-positives
+ if ($key != 'SUBJECT') {
+ $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
+ }
+ }
+
+ $search_str .= ' (' . implode(' ', $search_params) . ')';
+ }
+
+ $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
+
+ // search
+ $search = $storage->search_once($folders, $search_str);
+
+ // handle search result
+ $folders = (array) $search->get_parameters('MAILBOX');
+
+ foreach ($folders as $folder) {
+ $set = $search->get_set($folder);
+ $uids = $set->get();
+
+ if (!empty($uids)) {
+ $msgs = $storage->fetch_headers($folder, $uids, false);
+ $members = self::build_members($folder, $msgs);
+
+ // merge new members into the tag members list
+ $tag['members'] = array_merge($tag['members'], $members);
+
+ // add UIDs into the result
+ $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
+ }
+ }
+
+ // update tag object with new members list
+ $tag['members'] = array_unique($tag['members']);
+ kolab_storage_config::get_instance()->save($tag, 'relation', false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Assign tags to kolab objects
+ *
+ * @param array $records List of kolab objects
+ *
+ * @return array List of tags
+ */
+ public function apply_tags(&$records)
+ {
+ // first convert categories into tags
+ foreach ($records as $i => $rec) {
+ if (!empty($rec['categories'])) {
+ $folder = new kolab_storage_folder($rec['_mailbox']);
+ if ($object = $folder->get_object($rec['uid'])) {
+ $tags = $rec['categories'];
+
+ unset($object['categories']);
+ unset($records[$i]['categories']);
+
+ $this->save_tags($rec['uid'], $tags);
+ $folder->save($object, $rec['_type'], $rec['uid']);
+ }
+ }
+ }
+
+ $tags = array();
+
+ // assign tags to objects
+ foreach ($this->get_tags() as $tag) {
+ foreach ($records as $idx => $rec) {
+ $uid = self::build_member_url($rec['uid']);
+ if (in_array($uid, (array) $tag['members'])) {
+ $records[$idx]['tags'][] = $tag['name'];
+ }
+ }
+
+ $tags[] = $tag['name'];
+ }
+
+ $tags = array_unique($tags);
+
+ return $tags;
+ }
+
+ /**
+ * Update object tags
+ *
+ * @param string $uid Kolab object UID
+ * @param array $tags List of tag names
+ */
+ public function save_tags($uid, $tags)
+ {
+ $url = self::build_member_url($uid);
+ $relations = $this->get_tags();
+
+ foreach ($relations as $idx => $relation) {
+ $selected = !empty($tags) && in_array($relation['name'], $tags);
+ $found = !empty($relation['members']) && in_array($url, $relation['members']);
+ $update = false;
+
+ // remove member from the relation
+ if ($found && !$selected) {
+ $relation['members'] = array_diff($relation['members'], (array) $url);
+ $update = true;
+ }
+ // add member to the relation
+ else if (!$found && $selected) {
+ $relation['members'][] = $url;
+ $update = true;
+ }
+
+ if ($update) {
+ if ($this->save($relation, 'relation')) {
+ $this->tags[$idx] = $relation; // update in-memory cache
+ }
+ }
+
+ if ($selected) {
+ $tags = array_diff($tags, (array)$relation['name']);
+ }
+ }
+
+ // create new relations
+ if (!empty($tags)) {
+ foreach ($tags as $tag) {
+ $relation = array(
+ 'name' => $tag,
+ 'members' => (array) $url,
+ 'category' => 'tag',
+ );
+
+ if ($this->save($relation, 'relation')) {
+ $this->tags[] = $relation; // update in-memory cache
+ }
+ }
+ }
+ }
+
+ /**
+ * Get tags (all or referring to specified object)
+ *
+ * @param string $uid Optional object UID
+ *
+ * @return array List of Relation objects
+ */
+ public function get_tags($uid = '*')
+ {
+ if (!isset($this->tags)) {
+ $default = true;
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'tag')
+ );
+
+ // use faster method
+ if ($uid && $uid != '*') {
+ $filter[] = array('member', '=', $uid);
+ $tags = $this->get_objects($filter, $default);
+ }
+ else {
+ $this->tags = $tags = $this->get_objects($filter, $default);
+ }
+ }
+ else {
+ $tags = $this->tags;
+ }
+
+ if ($uid === '*') {
+ return $tags;
+ }
+
+ $result = array();
+ $search = self::build_member_url($uid);
+
+ foreach ($tags as $tag) {
+ if (in_array($search, (array) $tag['members'])) {
+ $result[] = $tag;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Find objects linked with the given groupware object through a relation
+ *
+ * @param string Object UUID
+ * @param array List of related URIs
+ */
+ public function get_object_links($uid)
+ {
+ $links = array();
+ $object_uri = self::build_member_url($uid);
+
+ foreach ($this->get_relations_for_member($uid) as $relation) {
+ if (in_array($object_uri, (array) $relation['members'])) {
+ // make relation members up-to-date
+ kolab_storage_config::resolve_members($relation);
+
+ foreach ($relation['members'] as $member) {
+ if ($member != $object_uri) {
+ $links[] = $member;
+ }
+ }
+ }
+ }
+
+ return array_unique($links);
+ }
+
+ /**
+ *
+ */
+ public function save_object_links($uid, $links, $remove = array())
+ {
+ $object_uri = self::build_member_url($uid);
+ $relations = $this->get_relations_for_member($uid);
+ $done = false;
+
+ foreach ($relations as $relation) {
+ // make relation members up-to-date
+ kolab_storage_config::resolve_members($relation);
+
+ // remove and add links
+ $members = array_diff($relation['members'], (array)$remove);
+ $members = array_unique(array_merge($members, $links));
+
+ // make sure the object_uri is still a member
+ if (!in_array($object_uri, $members)) {
+ $members[$object_uri];
+ }
+
+ // remove relation if no other members remain
+ if (count($members) <= 1) {
+ $done = $this->delete($relation['uid']);
+ }
+ // update relation object if members changed
+ else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
+ $relation['members'] = $members;
+ $done = $this->save($relation, 'relation');
+ $links = array();
+ }
+ // no changes, we're happy
+ else {
+ $done = true;
+ $links = array();
+ }
+ }
+
+ // create a new relation
+ if (!$done && !empty($links)) {
+ $relation = array(
+ 'members' => array_merge($links, array($object_uri)),
+ 'category' => 'generic',
+ );
+
+ $ret = $this->save($relation, 'relation');
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Find relation objects referring to specified note
+ */
+ public function get_relations_for_member($uid, $reltype = 'generic')
+ {
+ $default = true;
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', $reltype),
+ array('member', '=', $uid),
+ );
+
+ return $this->get_objects($filter, $default, 100);
+ }
+
+ /**
+ * Find kolab objects assigned to specified e-mail message
+ *
+ * @param rcube_message $message E-mail message
+ * @param string $folder Folder name
+ * @param string $type Result objects type
+ *
+ * @return array List of kolab objects
+ */
+ public function get_message_relations($message, $folder, $type)
+ {
+ static $_cache = array();
+
+ $result = array();
+ $uids = array();
+ $default = true;
+ $uri = self::get_message_uri($message, $folder);
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'generic'),
+ );
+
+ // query by message-id
+ $member_id = $message->get('message-id', false);
+ if (empty($member_id)) {
+ // derive message identifier from URI
+ $member_id = md5($uri);
+ }
+ $filter[] = array('member', '=', $member_id);
+
+ if (!isset($_cache[$uri])) {
+ // get UIDs of related groupware objects
+ foreach ($this->get_objects($filter, $default) as $relation) {
+ // we don't need to update members if the URI is found
+ if (!in_array($uri, $relation['members'])) {
+ // update members...
+ $messages = kolab_storage_config::resolve_members($relation);
+ // ...and check again
+ if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
+ continue;
+ }
+ }
+
+ // find groupware object UID(s)
+ foreach ($relation['members'] as $member) {
+ if (strpos($member, 'urn:uuid:') === 0) {
+ $uids[] = substr($member, 9);
+ }
+ }
+ }
+
+ // remember this lookup
+ $_cache[$uri] = $uids;
+ }
+ else {
+ $uids = $_cache[$uri];
+ }
+
+ // get kolab objects of specified type
+ if (!empty($uids)) {
+ $query = array(array('uid', '=', array_unique($uids)));
+ $result = kolab_storage::select($query, $type);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Build a URI representing the given message reference
+ */
+ public static function get_message_uri($headers, $folder)
+ {
+ $params = array(
+ 'folder' => $headers->folder ?: $folder,
+ 'uid' => $headers->uid,
+ );
+
+ if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
+ $params['message-id'] = $messageid;
+ $params['date'] = $date;
+
+ if ($subject = $headers->get('subject')) {
+ $params['subject'] = $subject;
+ }
+ }
+
+ return self::build_member_url($params);
+ }
+}