diff --git a/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php b/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php index 4112e9d9..9ec2d117 100644 --- a/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php +++ b/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php @@ -32,7 +32,7 @@ class carddav_contacts extends rcube_addressbook public $rights = 'lrs'; public $readonly = true; public $undelete = false; - public $groups = false; // TODO + public $groups = true; public $coltypes = [ 'name' => ['limit' => 1], @@ -108,7 +108,7 @@ class carddav_contacts extends rcube_addressbook 'manager', 'assistant', 'spouse', - 'children', +// 'children', 'notes', ]; @@ -810,30 +810,34 @@ class carddav_contacts extends rcube_addressbook function create_group($name) { $this->_fetch_groups(); - $result = false; + + $rcube = rcube::get_instance(); $list = [ - 'name' => $name, + 'uid' => strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($rcube->user->get_username()), 0, 16)), + 'name' => $name, + 'kind' => 'group', 'member' => [], ]; - $saved = $this->storage->save($list, 'distribution-list'); + + $saved = $this->storage->save($list, 'contact'); if (!$saved) { rcube::raise_error([ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Error saving distribution-list object to CardDAV server" + 'message' => "Error saving a contact group to CardDAV server" ], true, false ); + return false; } - else { - $id = $this->uid2id($list['uid']); - $this->distlists[$id] = $list; - $result = ['id' => $id, 'name' => $name]; - } - return $result; + $id = $this->uid2id($list['uid']); + + $this->distlists[$id] = $list; + + return ['id' => $id, 'name' => $name]; } /** @@ -846,25 +850,25 @@ class carddav_contacts extends rcube_addressbook function delete_group($gid) { $this->_fetch_groups(); - $result = false; - if ($list = $this->distlists[$gid]) { - $deleted = $this->storage->delete($list['uid']); + $list = $this->distlists[$gid]; + + if (!$list) { + return false; } + $deleted = $this->storage->delete($list['uid']); + if (!$deleted) { rcube::raise_error([ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Error deleting distribution-list object from the CardDAV server" + 'message' => "Error deleting a contact group from the CardDAV server" ], true, false ); } - else { - $result = true; - } - return $result; + return $deleted; } /** @@ -874,25 +878,33 @@ class carddav_contacts extends rcube_addressbook * @param string New name to set for this group * @param string New group identifier (if changed, otherwise don't set) * - * @return bool New name on success, false if no data was changed + * @return string|false New name on success, false if no data was changed */ function rename_group($gid, $newname, &$newid) { $this->_fetch_groups(); + $list = $this->distlists[$gid]; - if ($newname != $list['name']) { - $list['name'] = $newname; - $saved = $this->storage->save($list, 'distribution-list', $list['uid']); + if (!$list) { + return false; } + if ($newname === $list['name']) { + return $newname; + } + + $list['name'] = $newname; + $saved = $this->storage->save($list, 'contact', $list['uid']); + if (!$saved) { rcube::raise_error([ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Error saving distribution-list object to CardDAV server" + 'message' => "Error saving a contact group to CardDAV server" ], true, false ); + return false; } @@ -904,7 +916,8 @@ class carddav_contacts extends rcube_addressbook * * @param string Group identifier * @param array List of contact identifiers to be added - * @return int Number of contacts added + * + * @return int Number of contacts added */ function add_to_group($gid, $ids) { @@ -914,12 +927,17 @@ class carddav_contacts extends rcube_addressbook $this->_fetch_groups(true); - $list = $this->distlists[$gid]; + $list = $this->distlists[$gid]; + + if (!$list) { + return 0; + } + $added = 0; $uids = []; $exists = []; - foreach ((array)$list['member'] as $member) { + foreach ((array) $list['member'] as $member) { $exists[] = $member['ID']; } @@ -952,7 +970,7 @@ class carddav_contacts extends rcube_addressbook } if ($added) { - $saved = $this->storage->save($list, 'distribution-list', $list['uid']); + $saved = $this->storage->save($list, 'contact', $list['uid']); } else { $saved = true; @@ -961,7 +979,7 @@ class carddav_contacts extends rcube_addressbook if (!$saved) { rcube::raise_error([ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Error saving distribution-list to CardDAV server" + 'message' => "Error saving a contact-group to CardDAV server" ], true, false ); @@ -986,13 +1004,16 @@ class carddav_contacts extends rcube_addressbook */ function remove_from_group($gid, $ids) { - if (!is_array($ids)) { - $ids = explode(',', $ids); + $this->_fetch_groups(); + + $list = $this->distlists[$gid]; + + if (!$list) { + return false; } - $this->_fetch_groups(); - if (!($list = $this->distlists[$gid])) { - return false; + if (!is_array($ids)) { + $ids = explode(',', $ids); } $new_member = []; @@ -1004,12 +1025,12 @@ class carddav_contacts extends rcube_addressbook // write distribution list back to server $list['member'] = $new_member; - $saved = $this->storage->save($list, 'distribution-list', $list['uid']); + $saved = $this->storage->save($list, 'contact', $list['uid']); if (!$saved) { rcube::raise_error([ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Error saving distribution-list object to CardDAV server" + 'message' => "Error saving a contact group to CardDAV server" ], true, false ); @@ -1020,7 +1041,9 @@ class carddav_contacts extends rcube_addressbook $j = array_search($gid, $this->groupmembers[$id]); unset($this->groupmembers[$id][$j]); } + $this->distlists[$gid] = $list; + return true; } @@ -1130,15 +1153,17 @@ class carddav_contacts extends rcube_addressbook } /** - * Read distribution-lists AKA groups from server + * Read contact groups from server */ private function _fetch_groups($with_contacts = false) { - return; // TODO - if (!isset($this->distlists)) { $this->distlists = $this->groupmembers = []; - foreach ($this->storage->select('distribution-list', true) as $record) { + + // Set order (and LIMIT to skip the count(*) select) + $this->storage->set_order_and_limit(['name'], 200, 0); + + foreach ($this->storage->select('group', true) as $record) { $record['ID'] = $this->uid2id($record['uid']); foreach ((array)$record['member'] as $i => $member) { $mid = $this->uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']); @@ -1150,8 +1175,11 @@ class carddav_contacts extends rcube_addressbook $this->contacts[$mid] = $record['member'][$i]; } } + $this->distlists[$record['ID']] = $record; } + + $this->storage->set_order_and_limit($this->_sort_columns(), null, 0); } } diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php index fecff647..1837ff0a 100644 --- a/plugins/libkolab/lib/kolab_storage_dav.php +++ b/plugins/libkolab/lib/kolab_storage_dav.php @@ -55,7 +55,7 @@ class kolab_storage_dav /** * Get a list of storage folders for the given data type * - * @param string Data type to list folders for (contact,distribution-list,event,task,note) + * @param string Data type to list folders for (contact,event,task,note) * * @return array List of kolab_storage_dav_folder objects */ @@ -87,7 +87,7 @@ class kolab_storage_dav /** * Getter for the storage folder for the given type * - * @param string Data type to list folders for (contact,distribution-list,event,task,note) + * @param string Data type to list folders for (contact,event,task,note) * * @return object kolab_storage_dav_folder The folder object */ diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php index 0ca118dd..ed55e21e 100644 --- a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php +++ b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php @@ -38,6 +38,10 @@ class kolab_storage_dav_cache_contact extends kolab_storage_dav_cache $sql_data = parent::_serialize($object); $sql_data['type'] = $object['_type'] ?: 'contact'; + if ($sql_data['type'] == 'group' || (!empty($object['kind']) && $object['kind'] == 'group')) { + $sql_data['type'] = 'group'; + } + // columns for sorting $sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']); $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']); diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php index a588b95b..a8ea5794 100644 --- a/plugins/libkolab/lib/kolab_storage_dav_folder.php +++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php @@ -483,10 +483,52 @@ class kolab_storage_dav_folder extends kolab_storage_folder return false; } - $vcard = new rcube_vcard($object['data'], RCUBE_CHARSET, false); + // vCard properties not supported by rcube_vcard + $map = [ + 'uid' => 'UID', + 'kind' => 'KIND', + 'member' => 'MEMBER', + 'x-kind' => 'X-ADDRESSBOOKSERVER-KIND', + 'x-member' => 'X-ADDRESSBOOKSERVER-MEMBER', + ]; + + // TODO: We should probably use Sabre/Vobject to parse the vCard + + $vcard = new rcube_vcard($object['data'], RCUBE_CHARSET, false, $map); if (!empty($vcard->displayname) || !empty($vcard->surname) || !empty($vcard->firstname) || !empty($vcard->email)) { $result = $vcard->get_assoc(); + + // Contact groups + if (!empty($result['x-kind']) && implode($result['x-kind']) == 'group') { + $result['_type'] = 'group'; + $members = isset($result['x-member']) ? $result['x-member'] : []; + unset($result['x-kind'], $result['x-member']); + } + else if (!empty($result['kind']) && implode($result['kind']) == 'group') { + $result['_type'] = 'group'; + $members = isset($result['member']) ? $result['member'] : []; + unset($result['kind'], $result['member']); + } + + if (isset($members)) { + $result['member'] = []; + foreach ($members as $member) { + if (strpos($member, 'urn:uuid:') === 0) { + $result['member'][] = ['uid' => substr($member, 9)]; + } + else if (strpos($member, 'mailto:') === 0) { + $member = reset(rcube_mime::decode_address_list(urldecode(substr($member, 7)))); + if (!empty($member['mailto'])) { + $result['member'][] = ['email' => $member['mailto'], 'name' => $member['name']]; + } + } + } + } + + if (!empty($result['uid'])) { + $result['uid'] = preg_replace('/^urn:uuid:/', '', implode($result['uid'])); + } } else { return false; @@ -517,9 +559,17 @@ class kolab_storage_dav_folder extends kolab_storage_folder } else if ($this->type == 'contact') { // copy values into vcard object - $vcard = new rcube_vcard('', RCUBE_CHARSET, false, ['uid' => 'UID']); + // TODO: We should probably use Sabre/Vobject to create the vCard - $vcard->set('groups', null); + // vCard properties not supported by rcube_vcard + $map = ['uid' => 'UID', 'kind' => 'KIND']; + $vcard = new rcube_vcard('', RCUBE_CHARSET, false, $map); + + if ((!empty($object['_type']) && $object['_type'] == 'group') + || (!empty($object['type']) && $object['type'] == 'group') + ) { + $object['kind'] = 'group'; + } foreach ($object as $key => $values) { list($field, $section) = rcube_utils::explode(':', $key); @@ -537,6 +587,41 @@ class kolab_storage_dav_folder extends kolab_storage_folder } $result = $vcard->export(false); + + if (!empty($object['kind']) && $object['kind'] == 'group') { + $members = ''; + foreach ((array) $object['member'] as $member) { + $value = null; + if (!empty($member['uid'])) { + $value = 'urn:uuid:' . $member['uid']; + } + else if (!empty($member['email']) && !empty($member['name'])) { + $value = 'mailto:' . urlencode(sprintf('"%s" <%s>', addcslashes($member['name'], '"'), $member['email'])); + } + else if (!empty($member['email'])) { + $value = 'mailto:' . $member['email']; + } + + if ($value) { + $members .= "MEMBER:{$value}\r\n"; + } + } + + if ($members) { + $result = preg_replace('/\r\nEND:VCARD/', "\r\n{$members}END:VCARD", $result); + } + + /** + Version 4.0 of the vCard format requires Cyrus >= 3.6.0, we'll use Version 3.0 for now + + $result = preg_replace('/\r\nVERSION:3\.0\r\n/', "\r\nVERSION:4.0\r\n", $result); + $result = preg_replace('/\r\nN:[^\r]+/', '', $result); + $result = preg_replace('/\r\nUID:([^\r]+)/', "\r\nUID:urn:uuid:\\1", $result); + */ + + $result = preg_replace('/\r\nMEMBER:([^\r]+)/', "\r\nX-ADDRESSBOOKSERVER-MEMBER:\\1", $result); + $result = preg_replace('/\r\nKIND:([^\r]+)/', "\r\nX-ADDRESSBOOKSERVER-KIND:\\1", $result); + } } if ($result) { @@ -578,7 +663,7 @@ class kolab_storage_dav_folder extends kolab_storage_folder /** * Return folder name as string representation of this object * - * @return string Full IMAP folder name + * @return string Folder display name */ public function __toString() {