diff --git a/plugins/kolab_addressbook/config.inc.php.dist b/plugins/kolab_addressbook/config.inc.php.dist index ba8f60d1..bf4dc1d5 100644 --- a/plugins/kolab_addressbook/config.inc.php.dist +++ b/plugins/kolab_addressbook/config.inc.php.dist @@ -1,5 +1,10 @@ diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php index efbf74c8..a5c6e446 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.php +++ b/plugins/kolab_addressbook/kolab_addressbook.php @@ -31,12 +31,13 @@ class kolab_addressbook extends rcube_plugin { public $task = '?(?!logout).*'; + public $driver; + public $bonnie_api = false; + private $sources; - private $folders; private $rc; private $ui; - - public $bonnie_api = false; + private $driver_class; const GLOBAL_FIRST = 0; const PERSONAL_FIRST = 1; @@ -53,8 +54,11 @@ class kolab_addressbook extends rcube_plugin // load required plugin $this->require_plugin('libkolab'); - $driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab'; - require_once(dirname(__FILE__) . '/lib/rcube_' . $driver . '_contacts.php'); + $this->load_config(); + + $this->driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab'; + $this->driver_class = 'rcube_' . $this->driver . '_contacts'; + require_once(dirname(__FILE__) . '/lib/' . $this->driver_class . '.php'); // register hooks $this->add_hook('addressbooks_list', array($this, 'address_sources')); @@ -82,7 +86,6 @@ class kolab_addressbook extends rcube_plugin // Load UI elements if ($this->api->output->type == 'html') { - $this->load_config(); require_once($this->home . '/lib/kolab_addressbook_ui.php'); $this->ui = new kolab_addressbook_ui($this); @@ -104,9 +107,11 @@ class kolab_addressbook extends rcube_plugin $this->add_hook('preferences_save', array($this, 'prefs_save')); } - $this->add_hook('folder_delete', array($this, 'prefs_folder_delete')); - $this->add_hook('folder_rename', array($this, 'prefs_folder_rename')); - $this->add_hook('folder_update', array($this, 'prefs_folder_update')); + if ($this->driver == 'kolab') { + $this->add_hook('folder_delete', array($this, 'prefs_folder_delete')); + $this->add_hook('folder_rename', array($this, 'prefs_folder_rename')); + $this->add_hook('folder_update', array($this, 'prefs_folder_update')); + } } /** @@ -212,10 +217,22 @@ class kolab_addressbook extends rcube_plugin $out .= $this->addressbook_list_item($id, $source, $jsdata) . ''; } + $filter = function($source) { return !empty($source['kolab']) && empty($source['hidden']); }; + $folders = array_filter($sources, $filter); + // render a hierarchical list of kolab contact folders - kolab_storage::folder_hierarchy($this->folders, $tree); - if ($tree && !empty($tree->children)) { - $out .= $this->folder_tree_html($tree, $sources, $jsdata); + // TODO: Move this to the drivers + if ($this->driver == 'kolab') { + kolab_storage::folder_hierarchy($folders, $tree); + if ($tree && !empty($tree->children)) { + $out .= $this->folder_tree_html($tree, $sources, $jsdata); + } + } + else { + foreach ($folders as $j => $source) { + $id = strval(strlen($source['id']) ? $source['id'] : $j); + $out .= $this->addressbook_list_item($id, $source, $jsdata) . ''; + } } $this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return $src['type'] == 'group'; })); @@ -303,7 +320,7 @@ class kolab_addressbook extends rcube_plugin ), $name) ); - if (isset($source['subscribed'])) { + if ($this->driver == 'kolab' && isset($source['subscribed'])) { $inner .= html::span(array( 'class' => 'subscribed', 'title' => $this->gettext('foldersubscribe'), @@ -396,7 +413,6 @@ class kolab_addressbook extends rcube_plugin return $args; } - /** * Getter for the rcube_addressbook instance * @@ -407,16 +423,8 @@ class kolab_addressbook extends rcube_plugin public function get_address_book($p) { if ($p['id']) { - $id = kolab_storage::id_decode($p['id']); - $folder = kolab_storage::get_folder($id); - - // try with unencoded (old-style) identifier - if ((!$folder || $folder->type != 'contact') && $id != $p['id']) { - $folder = kolab_storage::get_folder($p['id']); - } - - if ($folder && $folder->type == 'contact') { - $p['instance'] = new rcube_kolab_contacts($folder->name); + if ($source = $this->driver_class::get_address_book($p['id'])) { + $p['instance'] = $source; // flag source as writeable if 'i' right is given if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) { @@ -431,57 +439,33 @@ class kolab_addressbook extends rcube_plugin return $p; } - + /** + * List addressbook sources list + */ private function _list_sources() { // already read sources - if (isset($this->sources)) + if (isset($this->sources)) { return $this->sources; + } - kolab_storage::$encode_ids = true; - $this->sources = array(); - $this->folders = array(); + $this->sources = []; $abook_prio = $this->addressbook_prio(); // Personal address source(s) disabled? - if ($abook_prio == self::GLOBAL_ONLY) { + if ($abook_prio == kolab_addressbook::GLOBAL_ONLY) { return $this->sources; } // get all folders that have "contact" type - $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact')); - - if (PEAR::isError($folders)) { - rcube::raise_error(array( - 'code' => 600, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage()), - true, false); - } - else { - // we need at least one folder to prevent from errors in Roundcube core - // when there's also no sql nor ldap addressbook (Bug #2086) - if (empty($folders)) { - if ($folder = kolab_storage::create_default_folder('contact')) { - $folders = array(new kolab_storage_folder($folder, 'contact')); - } - } - - // convert to UTF8 and sort - foreach ($folders as $folder) { - // create instance of rcube_contacts - $abook_id = $folder->id; - $abook = new rcube_kolab_contacts($folder->name); - $this->sources[$abook_id] = $abook; - $this->folders[$abook_id] = $folder; - } + foreach ($this->driver_class::list_folders() as $id => $source) { + $this->sources[$id] = $source; } return $this->sources; } - /** * Plugin hook called before rendering the contact form or detail view * @@ -801,17 +785,17 @@ class kolab_addressbook extends rcube_plugin */ private function _sort_form_fields($contents, $source) { - $block = array(); + $block = []; - foreach (array_keys($source->coltypes) as $col) { - if (isset($contents[$col])) - $block[$col] = $contents[$col]; - } + foreach (array_keys($source->coltypes) as $col) { + if (isset($contents[$col])) { + $block[$col] = $contents[$col]; + } + } - return $block; + return $block; } - /** * Handler for user preferences form (preferences_list hook) * @@ -959,9 +943,9 @@ class kolab_addressbook extends rcube_plugin */ public function book_search() { - $results = array(); - $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); - $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); + $results = []; + $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); + $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); kolab_storage::$encode_ids = true; $search_more_results = false; @@ -1109,12 +1093,6 @@ class kolab_addressbook extends rcube_plugin */ private function addressbook_prio() { - // Load configuration - if (!$this->config_loaded) { - $this->load_config(); - $this->config_loaded = true; - } - $abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio'); // Make sure any global addressbooks are defined diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php index 710b5562..b6bad178 100644 --- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php +++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php @@ -54,6 +54,10 @@ class kolab_addressbook_ui // Include stylesheet (for directorylist) $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); + if ($this->plugin->driver != 'kolab') { + return; + } + // include kolab folderlist widget if available if (in_array('libkolab', $this->plugin->api->loaded_plugins())) { $this->plugin->api->include_script('libkolab/libkolab.js'); @@ -66,20 +70,20 @@ class kolab_addressbook_ui $idx = 0; if ($dav_url = $this->rc->config->get('kolab_addressbook_carddav_url')) { - $options[] = 'book-showurl'; - $this->rc->output->set_env('kolab_addressbook_carddav_url', true); + $options[] = 'book-showurl'; + $this->rc->output->set_env('kolab_addressbook_carddav_url', true); - // set CardDAV URI for specified ldap addressbook - if ($ldap_abook = $this->rc->config->get('kolab_addressbook_carddav_ldap')) { - $dav_ldap_url = strtr($dav_url, array( - '%h' => $_SERVER['HTTP_HOST'], - '%u' => urlencode($this->rc->get_user_name()), - '%i' => 'ldap-directory', - '%n' => '', - )); - $this->rc->output->set_env('kolab_addressbook_carddav_ldap', $ldap_abook); - $this->rc->output->set_env('kolab_addressbook_carddav_ldap_url', $dav_ldap_url); - } + // set CardDAV URI for specified ldap addressbook + if ($ldap_abook = $this->rc->config->get('kolab_addressbook_carddav_ldap')) { + $dav_ldap_url = strtr($dav_url, array( + '%h' => $_SERVER['HTTP_HOST'], + '%u' => urlencode($this->rc->get_user_name()), + '%i' => 'ldap-directory', + '%n' => '', + )); + $this->rc->output->set_env('kolab_addressbook_carddav_ldap', $ldap_abook); + $this->rc->output->set_env('kolab_addressbook_carddav_ldap_url', $dav_ldap_url); + } } foreach ($options as $command) { diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php index 1f9ac291..be1a2b02 100644 --- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php +++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php @@ -312,6 +312,64 @@ class rcube_kolab_contacts extends rcube_addressbook $this->filter = null; } + /** + * List addressbook sources (folders) + */ + public static function list_folders() + { + kolab_storage::$encode_ids = true; + + // get all folders that have "contact" type + $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact')); + + if (PEAR::isError($folders)) { + rcube::raise_error([ + 'code' => 600, 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage() + ], + true, false); + + return []; + } + + // we need at least one folder to prevent from errors in Roundcube core + // when there's also no sql nor ldap addressbook (Bug #2086) + if (empty($folders)) { + if ($folder = kolab_storage::create_default_folder('contact')) { + $folders = [new kolab_storage_folder($folder, 'contact')]; + } + } + + $sources = []; + foreach ($folders as $folder) { + $sources[$folder->id] = new rcube_kolab_contacts($folder->name); + } + + return $sources; + } + + /** + * Getter for the rcube_addressbook instance + * + * @param string $id Addressbook (folder) ID + * + * @return ?rcube_kolab_contacts + */ + public static function get_address_book($id) + { + $folderId = kolab_storage::id_decode($id); + $folder = kolab_storage::get_folder($folderId); + + // try with unencoded (old-style) identifier + if ((!$folder || $folder->type != 'contact') && $folderId != $id) { + $folder = kolab_storage::get_folder($id); + } + + if ($folder && $folder->type == 'contact') { + return new rcube_kolab_contacts($folder->name); + } + } + /** * List all active contact groups of this source * diff --git a/plugins/libkolab/lib/kolab_dav_client.php b/plugins/libkolab/lib/kolab_dav_client.php index d9ae88da..e6962f53 100644 --- a/plugins/libkolab/lib/kolab_dav_client.php +++ b/plugins/libkolab/lib/kolab_dav_client.php @@ -73,7 +73,6 @@ class kolab_dav_client } try { - $request = $this->initRequest($this->url . $path, $method, $request_config); $request->setAuth($this->user, $this->password); @@ -135,7 +134,8 @@ class kolab_dav_client . '' . ''; - $response = $this->request('/' . $roots[$component], 'PROPFIND', $body); + // Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it) + $response = $this->request('/' . $roots[$component], 'PROPFIND', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']); $elements = $response->getElementsByTagName('response'); @@ -150,10 +150,22 @@ class kolab_dav_client $principal_href = substr($principal_href, strlen($path)); } + $homes = [ + 'VEVENT' => 'calendar-home-set', + 'VTODO' => 'calendar-home-set', + 'VCARD' => 'addressbook-home-set', + ]; + + $ns = [ + 'VEVENT' => 'caldav', + 'VTODO' => 'caldav', + 'VCARD' => 'carddav', + ]; + $body = '' - . '' + . '' . '' - . '' + . '' . '' . ''; @@ -178,28 +190,41 @@ class kolab_dav_client $root_href = '/' . $roots[$component] . '/' . rawurlencode($this->user); } + if ($component == 'VCARD') { + $add_ns = ''; + $add_props = ''; + } + else { + $add_ns = ' xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/"'; + $add_props = ''; + } + $body = '' - . '' + . '' . '' . '' . '' + // . '' . '' - . '' - . '' + . $add_props . '' . ''; - $response = $this->request($root_href, 'PROPFIND', $body); + // Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it) + $response = $this->request($root_href, 'PROPFIND', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']); if (empty($response)) { return false; } $folders = []; - foreach ($response->getElementsByTagName('response') as $element) { $folder = $this->getFolderPropertiesFromResponse($element); - if ($folder['type'] === $component) { + + // Note: Addressbooks don't have 'type' specified + if (($component == 'VCARD' && in_array('addressbook', $folder['resource_type'])) + || $folder['type'] === $component + ) { $folders[] = $folder; } } @@ -210,9 +235,17 @@ class kolab_dav_client /** * Create a DAV object in a folder */ - public function create($location, $content) + public function create($location, $content, $component = 'VEVENT') { - $response = $this->request($location, 'PUT', $content, ['Content-Type' => 'text/calendar; charset=utf-8']); + $ctype = [ + 'VEVENT' => 'text/calendar', + 'VTODO' => 'text/calendar', + 'VCARD' => 'text/vcard', + ]; + + $headers = ['Content-Type' => $ctype[$component] . '; charset=utf-8']; + + $response = $this->request($location, 'PUT', $content, $headers); if ($response !== false) { $etag = $this->responseHeaders['etag']; @@ -230,9 +263,9 @@ class kolab_dav_client /** * Update a DAV object in a folder */ - public function update($location, $content) + public function update($location, $content, $component = 'VEVENT') { - return $this->create($location, $content); + return $this->create($location, $content, $component); } /** diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php index ce064178..15295cfb 100644 --- a/plugins/libkolab/lib/kolab_storage_dav.php +++ b/plugins/libkolab/lib/kolab_storage_dav.php @@ -100,14 +100,18 @@ class kolab_storage_dav /** * Getter for a specific storage folder * - * @param string Folder to access - * @param string Expected folder type + * @param string $id Folder to access + * @param string $type Expected folder type * - * @return object kolab_storage_folder The folder object + * @return ?object kolab_storage_folder The folder object */ - public function get_folder($folder, $type = null) + public function get_folder($id, $type = null) { - // TODO + foreach ($this->get_folders($type) as $folder) { + if ($folder->id == $id) { + return $folder; + } + } } /** diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache.php b/plugins/libkolab/lib/kolab_storage_dav_cache.php index 837783b5..b089b170 100644 --- a/plugins/libkolab/lib/kolab_storage_dav_cache.php +++ b/plugins/libkolab/lib/kolab_storage_dav_cache.php @@ -319,8 +319,8 @@ class kolab_storage_dav_cache extends kolab_storage_cache $sql_data = $this->_serialize($object); $sql_data['folder_id'] = $this->folder_id; - $sql_data['uid'] = $object['uid']; - $sql_data['etag'] = $object['etag']; + $sql_data['uid'] = rcube_charset::clean($object['uid']); + $sql_data['etag'] = rcube_charset::clean($object['etag']); $args = []; $cols = ['folder_id', 'uid', 'etag', 'changed', 'data', 'tags', 'words']; @@ -524,8 +524,15 @@ class kolab_storage_dav_cache extends kolab_storage_cache // In Oracle we can't put long data inline, others we don't support yet if (strpos($this->db->db_provider, 'mysql') !== 0) { $extra_args = []; - $params = [$this->folder_id, $object['uid'], $object['etag'], $sql_data['changed'], - $sql_data['data'], $sql_data['tags'], $sql_data['words']]; + $params = [ + $this->folder_id, + rcube_charset::clean($object['uid']), + rcube_charset::clean($object['etag']), + $sql_data['changed'], + $sql_data['data'], + $sql_data['tags'], + $sql_data['words'] + ]; foreach ($this->extra_cols as $col) { $params[] = $sql_data[$col]; @@ -552,8 +559,8 @@ class kolab_storage_dav_cache extends kolab_storage_cache $values = array( $this->db->quote($this->folder_id), - $this->db->quote($object['uid']), - $this->db->quote($object['etag']), + $this->db->quote(rcube_charset::clean($object['uid'])), + $this->db->quote(rcube_charset::clean($object['etag'])), $this->db->now(), $this->db->quote($sql_data['changed']), $this->db->quote($sql_data['data']), diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php index 66d2b830..0ca118dd 100644 --- a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php +++ b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php @@ -21,12 +21,12 @@ * along with this program. If not, see . */ -class kolab_storage_cache_contact extends kolab_storage_cache +class kolab_storage_dav_cache_contact extends kolab_storage_dav_cache { protected $extra_cols_max = 255; protected $extra_cols = ['type', 'name', 'firstname', 'surname', 'email']; protected $data_props = ['type', 'name', 'firstname', 'middlename', 'prefix', 'suffix', 'surname', 'email', 'organization', 'member']; - protected $fulltext_cols = ['name', 'firstname', 'surname', 'middlename', 'email:address']; + protected $fulltext_cols = ['name', 'firstname', 'surname', 'middlename', 'email']; /** * Helper method to convert the given Kolab object into a dataset to be written to cache @@ -36,20 +36,20 @@ class kolab_storage_cache_contact extends kolab_storage_cache protected function _serialize($object) { $sql_data = parent::_serialize($object); - $sql_data['type'] = $object['_type']; + $sql_data['type'] = $object['_type'] ?: 'contact'; // 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']); $sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']); - $sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']); + $sql_data['email'] = ''; - if (is_array($sql_data['email'])) { - $sql_data['email'] = $sql_data['email']['address']; - } - // avoid value being null - if (empty($sql_data['email'])) { - $sql_data['email'] = ''; + foreach ($object as $colname => $value) { + list($col, $field) = explode(':', $colname); + if ($col == 'email' && !empty($value)) { + $sql_data['email'] = is_array($value) ? $value[0] : $value; + break; + } } // use organization if name is empty @@ -78,21 +78,18 @@ class kolab_storage_cache_contact extends kolab_storage_cache public function get_words($object) { $data = ''; - foreach ($this->fulltext_cols as $colname) { + + foreach ($object as $colname => $value) { list($col, $field) = explode(':', $colname); - if ($field) { - $a = []; - foreach ((array)$object[$col] as $attr) - $a[] = $attr[$field]; - $val = join(' ', $a); - } - else { - $val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col]; + $val = ''; + if (in_array($col, $this->fulltext_cols)) { + $val = is_array($value) ? join(' ', $value) : $value; } - if (strlen($val)) + if (strlen($val)) { $data .= $val . ' '; + } } return array_unique(rcube_utils::normalize_string($data, true)); diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php index 32ecdd5d..c7a46f47 100644 --- a/plugins/libkolab/lib/kolab_storage_dav_folder.php +++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php @@ -32,12 +32,9 @@ class kolab_storage_dav_folder extends kolab_storage_folder public function __construct($dav, $attributes, $type_annotation = '') { $this->attributes = $attributes; - $this->href = $this->attributes['href']; - // Here we assume the last element of the folder path is the folder ID - // if that's not the case, we should consider generating an ID - $href = explode('/', unslashify($this->href)); - $this->id = $href[count($href) - 1]; + $this->href = $this->attributes['href']; + $this->id = md5($this->href); $this->dav = $dav; $this->valid = true; @@ -410,8 +407,9 @@ class kolab_storage_dav_folder extends kolab_storage_folder // generate and save object message if ($content = $this->to_dav($object)) { - $method = $uid ? 'update' : 'create'; - $result = $this->dav->{$method}($this->object_location($object['uid']), $content); + $method = $uid ? 'update' : 'create'; + $dav_type = $this->get_dav_type(); + $result = $this->dav->{$method}($this->object_location($object['uid']), $content, $dav_type); // Note: $result can be NULL if the request was successful, but ETag wasn't returned if ($result !== false) { @@ -473,10 +471,24 @@ class kolab_storage_dav_folder extends kolab_storage_folder $result = $events[0]; } + else if ($this->type == 'contact') { + if (stripos($object['data'], 'BEGIN:VCARD') !== 0) { + return false; + } + + $vcard = new rcube_vcard($object['data'], RCUBE_CHARSET, false); + + if (!empty($vcard->displayname) || !empty($vcard->surname) || !empty($vcard->firstname) || !empty($vcard->email)) { + $result = $vcard->get_assoc(); + } + else { + return false; + } + } $result['etag'] = $object['etag']; $result['href'] = $object['href']; - $result['uid'] = $object['uid'] ?: $result['uid']; + $result['uid'] = $object['uid'] ?: $result['uid']; return $result; } @@ -496,6 +508,35 @@ class kolab_storage_dav_folder extends kolab_storage_folder $result = $ical->export([$object]); } + else if ($this->type == 'contact') { + // copy values into vcard object + $vcard = new rcube_vcard('', RCUBE_CHARSET, false, ['uid' => 'UID']); + + $vcard->set('groups', null); + + foreach ($object as $key => $values) { + list($field, $section) = rcube_utils::explode(':', $key); + + // avoid casting DateTime objects to array + if (is_object($values) && is_a($values, 'DateTime')) { + $values = [$values]; + } + + foreach ((array) $values as $value) { + if (isset($value)) { + $vcard->set($field, $value, $section); + } + } + } + + $result = $vcard->export(false); + } + + if ($result) { + // The content must be UTF-8, otherwise if we try to fetch the object + // from server XML parsing would fail. + $result = rcube_charset::clean($result); + } return $result; }