Projects
Kolab:Winterfell
kolab-syncroton
0001-T2477-GAL-for-Outlook.patch
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0001-T2477-GAL-for-Outlook.patch of Package kolab-syncroton (Revision 21)
Currently displaying revision
21
,
Show latest
From 3695940a9949322c6ea9b6b71d8319ada8d84d9e Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <machniak@kolabsys.com> Date: Fri, 16 Jun 2017 08:42:27 +0000 Subject: [PATCH 1/2] T2477: GAL for Outlook --- config/config.inc.php.dist | 24 +++ lib/kolab_sync_data.php | 44 +++-- lib/kolab_sync_data_contacts.php | 360 ++++++++++++++++++++++++++++++++++++++- lib/kolab_sync_data_gal.php | 35 ++-- 4 files changed, 421 insertions(+), 42 deletions(-) diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist index 6864820..2433475 100644 --- a/config/config.inc.php.dist +++ b/config/config.inc.php.dist @@ -52,6 +52,20 @@ $config['activesync_addressbooks'] = array(); */ $config['activesync_gal_fieldmap'] = null; +// List of device types that will sync the LDAP addressbook(s) as a normal folder. +// For devices that do not support GAL searching, e.g. Outlook. +// Examples: +// array('windowsoutlook') # enable for Oultook only +// true # enable for all +$config['activesync_gal_sync'] = false; + +// GAL cache. As reading all contacts from LDAP may be slow, caching is recommended. +$config['activesync_gal_cache'] = 'db'; + +// TTL of GAL cache entries. Technically this causes that synchronized +// contacts will not be updated (queried) often than the specified interval. +$config['activesync_gal_cache_ttl'] = '1d'; + // List of Roundcube plugins // WARNING: Not all plugins used in Roundcube can be listed here $config['activesync_plugins'] = array(); @@ -89,6 +103,16 @@ $config['activesync_init_subscriptions'] = 0; // action and enable folder hierarchies only on device types known to support it. $config['activesync_multifolder_blacklist'] = null; +// Blacklist overwrites for specified object type. If set to an array +// it will have a precedence over 'activesync_multifolder_blacklist' list only for that type. +// Note: Outlook does not support multiple folders for contacts, +// in that case use $config['activesync_multifolder_blacklist_contact'] = array('windowsoutlook'); +$config['activesync_multifolder_blacklist_mail'] = null; +$config['activesync_multifolder_blacklist_event'] = null; +$config['activesync_multifolder_blacklist_contact'] = null; +$config['activesync_multifolder_blacklist_note'] = null; +$config['activesync_multifolder_blacklist_task'] = null; + // Enables adding sender name in the From: header of send email // when a device uses email address only (e.g. iOS devices) $config['activesync_fix_from'] = false; diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php index a32e738..b03d04b 100644 --- a/lib/kolab_sync_data.php +++ b/lib/kolab_sync_data.php @@ -247,16 +247,18 @@ abstract class kolab_sync_data implements Syncroton_Data_IData */ protected function isMultiFolder() { - $blacklist = rcube::get_instance()->config->get('activesync_multifolder_blacklist'); + $config = rcube::get_instance()->config; + $blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName); - if (is_array($blacklist)) { - $is_multifolder = !in_array_nocase($this->device->devicetype, $blacklist); + if (!is_array($blacklist)) { + $blacklist = $config->get('activesync_multifolder_blacklist'); } - else { - $is_multifolder = in_array_nocase($this->device->devicetype, $this->ext_devices); + + if (is_array($blacklist)) { + return !$this->deviceTypeFilter($blacklist); } - return $is_multifolder; + return in_array_nocase($this->device->devicetype, $this->ext_devices); } /** @@ -908,17 +910,6 @@ abstract class kolab_sync_data implements Syncroton_Data_IData */ public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState) { - // Try to detect change in multi-folder mode and throw exception - // so device will re-sync folders hierarchy - // @TODO: this is a temp solution until we have real hierarchy - // changes detection fort Ping/Hartbeat - $is_multifolder = $this->isMultiFolder(); - if (($is_multifolder && $folder->serverId == $this->defaultRootFolder) - || (!$is_multifolder && $folder->type >= 12) - ) { - throw new Syncroton_Exception_NotFound('Folder not found'); - } - try { if ($this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype)) { return true; @@ -1819,4 +1810,23 @@ abstract class kolab_sync_data implements Syncroton_Data_IData return $result; } + + /** + * Check if current device type string matches any of options + */ + protected function deviceTypeFilter($options) + { + foreach ($options as $option) { + if ($option[0] == '/') { + if (preg_match($option, $this->device->devicetype)) { + return true; + } + } + else if (stripos($this->device->devicetype, $option) !== false) { + return true; + } + } + + return false; + } } diff --git a/lib/kolab_sync_data_contacts.php b/lib/kolab_sync_data_contacts.php index e348474..0562377 100644 --- a/lib/kolab_sync_data_contacts.php +++ b/lib/kolab_sync_data_contacts.php @@ -4,7 +4,7 @@ +--------------------------------------------------------------------------+ | Kolab Sync (ActiveSync for Kolab) | | | - | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> | + | Copyright (C) 2011-2017, 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 | @@ -124,6 +124,25 @@ class kolab_sync_data_contacts extends kolab_sync_data */ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; + /** + * Identifier of special Global Address List folder + * + * @var string + */ + protected $galFolder = 'GAL'; + + /** + * Name of special Global Address List folder + * + * @var string + */ + protected $galFolderName = 'Global Address Book'; + + protected $galPrefix = 'GAL:'; + protected $galSources; + protected $galResult; + protected $galCache; + /** * Creates model object @@ -136,6 +155,10 @@ class kolab_sync_data_contacts extends kolab_sync_data $data = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId); $result = array(); + if (empty($data)) { + throw new Syncroton_Exception_NotFound("Contact $serverId not found"); + } + // Contacts namespace fields foreach ($this->mapping as $key => $name) { $value = $this->getKolabDataItem($data, $name); @@ -169,8 +192,13 @@ class kolab_sync_data_contacts extends kolab_sync_data // email address(es): email1Address, email2Address, email3Address for ($x=0; $x<3; $x++) { - if (!empty($data['email'][$x]) && !empty($data['email'][$x]['address'])) { - $result['email' . ($x+1) . 'Address'] = $data['email'][$x]['address']; + if ($email = $data['email'][$x]) { + if (is_array($email)) { + $email = $email['address']; + } + if ($email) { + $result['email' . ($x+1) . 'Address'] = $email; + } } } @@ -267,6 +295,148 @@ class kolab_sync_data_contacts extends kolab_sync_data } /** + * Return list of supported folders for this backend + * + * @return array + */ + public function getAllFolders() + { + $list = parent::getAllFolders(); + + if ($this->isMultiFolder() && $this->hasGAL()) { + $list[$this->galFolder] = new Syncroton_Model_Folder(array( + 'displayName' => $this->galFolderName, // @TODO: localization? + 'serverId' => $this->galFolder, + 'parentId' => 0, + 'type' => 14, + )); + } + + return $list; + } + + /** + * Updates a folder + */ + public function updateFolder(Syncroton_Model_IFolder $folder) + { + if ($folder->serverId === $this->galFolder && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Updating GAL folder is not possible"); + } + + return parent::updateFolder($folder); + } + + /** + * Deletes a folder + */ + public function deleteFolder($folder) + { + if ($folder instanceof Syncroton_Model_IFolder) { + $folder = $folder->serverId; + } + + if ($folder === $this->galFolder && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Deleting GAL folder is not possible"); + } + + return parent::deleteFolder($folder); + } + + /** + * Empty folder (remove all entries and optionally subfolders) + * + * @param string $folderId Folder identifier + * @param array $options Options + */ + public function emptyFolderContents($folderid, $options) + { + if ($folderid === $this->galFolder && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Emptying GAL folder is not possible"); + } + + return parent::emptyFolderContents($folderid, $options); + } + + /** + * Moves object into another location (folder) + * + * @param string $srcFolderId Source folder identifier + * @param string $serverId Object identifier + * @param string $dstFolderId Destination folder identifier + * + * @throws Syncroton_Exception_Status + * @return string New object identifier + */ + public function moveItem($srcFolderId, $serverId, $dstFolderId) + { + if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Moving GAL entries is not possible"); + } + + if ($srcFolderId === $this->galFolder && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Moving/Deleting GAL entries is not possible"); + } + + if ($dstFolderId === $this->galFolder && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Creating GAL entries is not possible"); + } + + return parent::moveItem($srcFolderId, $serverId, $dstFolderId); + } + + /** + * Add entry + * + * @param string $folderId Folder identifier + * @param Syncroton_Model_IEntry $entry Entry object + * + * @return string ID of the created entry + */ + public function createEntry($folderId, Syncroton_Model_IEntry $entry) + { + if ($folderId === $this->galFolder && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Creating GAL entries is not possible"); + } + + return parent::createEntry($folderId, $entry); + } + + /** + * update existing entry + * + * @param string $folderId + * @param string $serverId + * @param SimpleXMLElement $entry + * + * @return string ID of the updated entry + */ + public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) + { + if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Updating GAL entries is not possible"); + } + + return parent::updateEntry($folderId, $serverId, $entry); + } + + /** + * delete entry + * + * @param string $folderId + * @param string $serverId + * @param array $collectionData + */ + public function deleteEntry($folderId, $serverId, $collectionData) + { + if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) { + throw new Syncroton_Exception_AccessDenied("Deleting GAL entries is not possible"); + } + + return parent::deleteEntry($folderId, $serverId, $collectionData); + } + + /** * Returns filter query array according to specified ActiveSync FilterType * * @param int $filter_type Filter type @@ -280,4 +450,188 @@ class kolab_sync_data_contacts extends kolab_sync_data return array(array('type', '=', $this->modelName)); } + /** + * Check if GAL synchronization is enabled for current device + */ + protected function hasGAL() + { + return count($this->getGALSources()); + } + + /** + * Search for existing entries + * + * @param string $folderid Folder identifier + * @param array $filter Search filter + * @param int $result_type Type of the result (see RESULT_* constants) + * + * @return array|int Search result as count or array of uids/objects + */ + protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID) + { + // GAL Folder exists, return result from LDAP only + if ($folderid === $this->galFolder && $this->hasGAL()) { + return $this->searchGALEntries($filter, $result_type); + } + + $result = parent::searchEntries($folderid, $filter, $result_type); + + // Merge results from LDAP + if ($this->hasGAL() && !$this->isMultiFolder()) { + $gal_result = $this->searchGALEntries($filter, $result_type); + + if ($result_type == self::RESULT_COUNT) { + $result += $gal_result; + } + else { + $result = array_merge($result, $gal_result); + } + } + + return $result; + } + + /** + * Fetches the entry from the backend + */ + protected function getObject($folderid, $entryid, &$folder = null) + { + if (strpos($entryid, $this->galPrefix) === 0 && $this->hasGAL()) { + return $this->getGALEntry($entryid); + } + + return parent::getObject($folderid, $entryid, $folder); + } + + /** + * Search for existing LDAP entries + * + * @param array $filter Search filter + * @param int $result_type Type of the result (see RESULT_* constants) + * + * @return array|int Search result as count or array of uids/objects + */ + protected function searchGALEntries($filter, $result_type) + { + // For GAL we don't check for changes. + // When something changed a new UID will be generated so the update + // will be done as delete + create + foreach ($filter as $f) { + if ($f[0] == 'changed') { + return $result_type == self::RESULT_COUNT ? 0 : array(); + } + } + + if ($this->galCache && ($result = $this->galCache->get('index')) !== null) { + $result = explode("\n", $result); + return $result_type == self::RESULT_COUNT ? count($result) : $result; + } + + $result = array(); + + foreach ($this->getGALSources() as $source) { + if ($book = kolab_sync_data_gal::get_address_book($source['id'])) { + $book->reset(); + $book->set_page(1); + $book->set_pagesize(10000); + + $set = $book->list_records(); + while ($contact = $set->next()) { + $result[] = $this->createGALEntryUID($contact, $source['id']); + } + } + } + + if ($this->galCache) { + $this->galCache->set('index', implode("\n", $result)); + } + + return $result_type == self::RESULT_COUNT ? count($result) : $result; + } + + /** + * Return specified LDAP entry + * + * @param string $serverId Entry identifier + * + * @return array Contact data + */ + protected function getGALEntry($serverId) + { + list($source, $timestamp, $uid) = $this->resolveGALEntryUID($serverId); + + if ($source && $uid && ($book = kolab_sync_data_gal::get_address_book($source))) { + $book->reset(); + + $set = $book->search('uid', array($uid), rcube_addressbook::SEARCH_STRICT, true, true); + $result = $set->first(); + + if ($result['uid'] == $uid && $result['changed'] == $timestamp) { + // As in kolab_sync_data_gal we use only one email address + if (empty($result['email'])) { + $emails = $book->get_col_values('email', $result, true); + $result['email'] = array($emails[0]); + } + + return $result; + } + } + } + + /** + * Return LDAP address books list + * + * @return array Address books array + */ + protected function getGALSources() + { + if ($this->galSources === null) { + $rcube = rcube::get_instance(); + $gal_sync = $rcube->config->get('activesync_gal_sync'); + $enabled = false; + + if ($gal_sync === true) { + $enabled = true; + } + else if (is_array($gal_sync)) { + $enabled = $this->deviceTypeFilter($gal_sync); + } + + $this->galSources = $enabled ? kolab_sync_data_gal::get_address_sources() : array(); + + if ($cache_type = $rcube->config->get('activesync_gal_cache', 'db')) { + $cache_ttl = $rcube->config->get('activesync_gal_cache_ttl', '1d'); + $this->galCache = $rcube->get_cache('activesync_gal', $cache_type, $cache_ttl, false); + + // expunge cache every now and then + if (rand(0, 10) === 0) { + $this->galCache->expunge(); + } + } + } + + return $this->galSources; + } + + /** + * Builds contact identifier from contact data and source id + */ + protected function createGALEntryUID($contact, $source_id) + { + return $this->galPrefix . sprintf('%s:%s:%s', rcube_ldap::dn_encode($source_id), $contact['changed'], $contact['uid']); + } + + /** + * Extracts contact identification data from contact identifier + */ + protected function resolveGALEntryUID($uid) + { + if (strpos($uid, $this->galPrefix) === 0) { + $items = explode(':', substr($uid, strlen($this->galPrefix))); + $items[0] = rcube_ldap::dn_decode($items[0]); + return $items; // source, timestamp, uid + } + + return array(); + } } diff --git a/lib/kolab_sync_data_gal.php b/lib/kolab_sync_data_gal.php index 297a4a9..95894e7 100644 --- a/lib/kolab_sync_data_gal.php +++ b/lib/kolab_sync_data_gal.php @@ -42,7 +42,7 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat * * @var array */ - protected $address_books = array(); + protected static $address_books = array(); /** * Mapping from ActiveSync Contacts namespace fields @@ -193,7 +193,7 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat // @TODO: caching with Options->RebuildResults support - $books = $this->get_address_sources(); + $books = self::get_address_sources(); $mode = 2; // use prefix mode $fields = $rcube->config->get('contactlist_fields'); @@ -202,7 +202,7 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat } foreach ($books as $idx => $book) { - $book = $this->get_address_book($idx); + $book = self::get_address_book($idx); if (!$book) { continue; @@ -284,14 +284,14 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat * * @return rcube_contacts Address book object */ - protected function get_address_book($id) + public static function get_address_book($id) { $config = rcube::get_instance()->config; $ldap_config = (array) $config->get('ldap_public'); // use existing instance - if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) { - $book = $this->address_books[$id]; + if (isset(self::$address_books[$id]) && (self::$address_books[$id] instanceof rcube_addressbook)) { + $book = self::$address_books[$id]; } else if ($id && $ldap_config[$id]) { $book = new rcube_ldap($ldap_config[$id], $config->get('ldap_debug'), @@ -313,7 +313,7 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat $book->set_sort_order($sort_col); */ // add to the 'books' array for shutdown function - $this->address_books[$id] = $book; + self::$address_books[$id] = $book; return $book; } @@ -324,7 +324,7 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat * * @return array Address books array */ - protected function get_address_sources() + public static function get_address_sources() { $config = rcube::get_instance()->config; $ldap_config = (array) $config->get('ldap_public'); @@ -338,21 +338,12 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat foreach ((array)$async_books as $id) { $prop = $ldap_config[$id]; - // handle misconfiguration - if (empty($prop) || !is_array($prop)) { - continue; + if (!empty($prop) && is_array($prop)) { + $list[$id] = array( + 'id' => $id, + 'name' => $prop['name'], + ); } - - $list[$id] = array( - 'id' => $id, - 'name' => $prop['name'], - ); -/* - // register source for shutdown function - if (!is_object($this->address_books[$id])) - $this->address_books[$id] = $list[$id]; - } -*/ } return $list; -- 2.13.0
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.