Projects
Kolab:16
kolab-syncroton
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 13
View file
kolab-syncroton.spec
Changed
@@ -36,7 +36,7 @@ %global _ap_sysconfdir %{_sysconfdir}/%{httpd_name} Name: kolab-syncroton -Version: 2.3.5 +Version: 2.3.6 Release: 1%{?dist} Summary: ActiveSync for Kolab Groupware @@ -47,11 +47,6 @@ Source0: https://mirror.kolabenterprise.com/pub/releases/%{name}-%{version}.tar.gz Source1: kolab-syncroton.logrotate -Patch0001: 0001-T2477-GAL-for-Outlook.patch -Patch0002: 0002-Fix-MeetingStatus-value-Bifrost-T34257.patch -Patch0003: 0003-Add-important-note-about-uid-and-changed-fields-in-G.patch -Patch0004: 0004-T2519-Fix-Recurrence-element-structure.patch - BuildArch: noarch # Use this build requirement to make sure we are using @@ -97,11 +92,6 @@ %prep %setup -q -n %{name}-%{version} -%patch0001 -p1 -%patch0002 -p1 -%patch0003 -p1 -%patch0004 -p1 - %build %install @@ -232,6 +222,9 @@ %attr(0770,%{httpd_user},%{httpd_group}) %{_var}/log/%{name} %changelog +* Wed Jul 19 2017 Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> - 2.3.6-1 +- Release 2.3.6 + * Sun Jun 18 2017 Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> - 2.3.5-2 - Implement a GAL virtual folder with LDAP backend for Outlook over Activesync - Fix organizer / ownership for events
View file
0001-T2477-GAL-for-Outlook.patch
Deleted
@@ -1,626 +0,0 @@ -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/4] 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 -
View file
0002-Fix-MeetingStatus-value-Bifrost-T34257.patch
Deleted
@@ -1,71 +0,0 @@ -From 07a86bbd51c6444135eb8ed7ded465aad64167eb Mon Sep 17 00:00:00 2001 -From: Aleksander Machniak <machniak@kolabsys.com> -Date: Fri, 16 Jun 2017 15:07:10 +0000 -Subject: [PATCH 2/4] Fix MeetingStatus value (Bifrost#T34257) - -Wrong value caused Outlook to think every event with attendees is organized -by the current user even if he was an attendee not organizer. ---- - lib/kolab_sync_data_calendar.php | 39 ++++++++++++++++++++++++++++++++++++++- - 1 file changed, 38 insertions(+), 1 deletion(-) - -diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php -index 14d929f..fcfd51f 100644 ---- a/lib/kolab_sync_data_calendar.php -+++ b/lib/kolab_sync_data_calendar.php -@@ -308,7 +308,7 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data - } - - // Event meeting status -- $result['meetingStatus'] = intval(!empty($result['attendees'])); -+ $this->meeting_status_from_kolab($collection, $event, $result); - - // Recurrence (and exceptions) - $this->recurrence_from_kolab($collection, $event, $result); -@@ -543,6 +543,43 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data - } - - /** -+ * Set MeetingStatus according to event data -+ */ -+ protected function meeting_status_from_kolab($collection, $event, &$result) -+ { -+ // 0 - The event is an appointment, which has no attendees. -+ // 1 - The event is a meeting and the user is the meeting organizer. -+ // 3 - This event is a meeting, and the user is not the meeting organizer. -+ // 5 - The meeting has been canceled and the user was the meeting organizer. -+ // 7 - The meeting has been canceled. The user was not the meeting organizer. -+ $status = 0; -+ -+ if (!empty($event['attendees'])) { -+ // Find out if the user is an organizer -+ // TODO: Delegation/aliases support -+ $user_emails = kolab_sync::get_instance()->user->list_emails(); -+ $user_emails = array_map(function($v) { return $v['email']; }, $user_emails); -+ $is_organizer = true; -+ -+ foreach ($event['attendees'] as $attendee) { -+ if (in_array_nocase($attendee['email'], $user_emails)) { -+ $is_organizer = false; -+ break; -+ } -+ } -+ -+ if ($event['status'] == 'CANCELLED') { -+ $status = !empty($is_organizer) ? 5 : 7; -+ } -+ else { -+ $status = !empty($is_organizer) ? 1 : 3; -+ } -+ } -+ -+ $result['meetingStatus'] = $status; -+ } -+ -+ /** - * Converts libkolab alarms spec. into a number of minutes - */ - protected function from_kolab_alarm($event) --- -2.13.0 -
View file
0003-Add-important-note-about-uid-and-changed-fields-in-G.patch
Deleted
@@ -1,29 +0,0 @@ -From c2cdb863178c682f544ffbfa9d7d1635c3714de2 Mon Sep 17 00:00:00 2001 -From: Aleksander Machniak <machniak@kolabsys.com> -Date: Tue, 20 Jun 2017 10:46:35 +0200 -Subject: [PATCH 3/4] Add important note about uid and changed fields in GAL - config - ---- - config/config.inc.php.dist | 5 +++++ - 1 file changed, 5 insertions(+) - -diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist -index 2433475..5e66f59 100644 ---- a/config/config.inc.php.dist -+++ b/config/config.inc.php.dist -@@ -54,6 +54,11 @@ $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. -+// Note: To make the LDAP addressbook sources working we need two additional -+// fields ('uid' and 'changed') specified in the fieldmap array -+// of the LDAP configuration ('ldap_public' option). For example: -+// 'uid' => 'nsuniqueid', -+// 'changed' => 'modifytimestamp', - // Examples: - // array('windowsoutlook') # enable for Oultook only - // true # enable for all --- -2.13.0 -
View file
0004-T2519-Fix-Recurrence-element-structure.patch
Deleted
@@ -1,32 +0,0 @@ -From 4e805ba57d9911e2382c11fb043d6b827de7e369 Mon Sep 17 00:00:00 2001 -From: Aleksander Machniak <machniak@kolabsys.com> -Date: Tue, 27 Jun 2017 10:14:27 +0000 -Subject: [PATCH 4/4] T2519: Fix Recurrence element structure - -For example DayOfWeek=0 is invalid. We make sure no such "empty" values are set. ---- - lib/kolab_sync_data.php | 7 ++++++- - 1 file changed, 6 insertions(+), 1 deletion(-) - -diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php -index b03d04b..c03be25 100644 ---- a/lib/kolab_sync_data.php -+++ b/lib/kolab_sync_data.php -@@ -1562,8 +1562,13 @@ abstract class kolab_sync_data implements Syncroton_Data_IData - break; - } - -+ // Skip all empty values (T2519) -+ if ($recurrence['type'] != self::RECUR_TYPE_DAILY) { -+ $recurrence = array_filter($recurrence); -+ } -+ - // required field -- $recurrence['interval'] = $r['INTERVAL'] ? $r['INTERVAL'] : 1; -+ $recurrence['interval'] = $r['INTERVAL'] ?: 1; - - if (!empty($r['UNTIL'])) { - $recurrence['until'] = self::date_from_kolab($r['UNTIL']); --- -2.13.0 -
View file
debian.changelog
Changed
@@ -1,3 +1,9 @@ +kolab-syncroton (2.3.6-0~kolab1) unstable; urgency=low + + * Release 2.3.6 + + -- Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> Wed, 19 Jul 2017 15:13:40 +0200 + kolab-syncroton (2.3.5-0~kolab4) unstable; urgency=low * Allow a GAL to be distributed from LDAP, for Outlook over Activesync
View file
debian.series
Changed
@@ -1,4 +0,0 @@ -0001-T2477-GAL-for-Outlook.patch -p1 -0002-Fix-MeetingStatus-value-Bifrost-T34257.patch -p1 -0003-Add-important-note-about-uid-and-changed-fields-in-G.patch -p1 -0004-T2519-Fix-Recurrence-element-structure.patch -p1
View file
kolab-syncroton-2.3.5.tar.gz/config/config.inc.php.dist -> kolab-syncroton-2.3.6.tar.gz/config/config.inc.php.dist
Changed
@@ -52,6 +52,25 @@ */ $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. +// Note: To make the LDAP addressbook sources working we need two additional +// fields ('uid' and 'changed') specified in the fieldmap array +// of the LDAP configuration ('ldap_public' option). For example: +// 'uid' => 'nsuniqueid', +// 'changed' => 'modifytimestamp', +// 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 +108,16 @@ // 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;
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync.php
Changed
@@ -46,7 +46,7 @@ public $password; const CHARSET = 'UTF-8'; - const VERSION = "2.3.5"; + const VERSION = "2.3.6"; /** @@ -389,7 +389,7 @@ $this->logger->set_username($username); - $user_debug = $this->config->get('activesync_user_debug'); + $user_debug = $this->config->get('per_user_logging'); $user_log = $user_debug || $this->config->get('activesync_user_log'); if (!$user_log) {
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync_data.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync_data.php
Changed
@@ -247,16 +247,18 @@ */ 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); } /** @@ -482,7 +484,7 @@ throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } - return $entry['uid']; + return $entry['_serverId']; } /** @@ -503,13 +505,13 @@ } $entry = $this->toKolab($entry, $folderId, $oldEntry); - $entry = $this->updateObject($folderId, $serverId, $entry); + $entry = $this->updateObject($folderId, $serverId, $entry); if (empty($entry)) { throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); } - return $entry['uid']; + return $entry['_serverId']; } /** @@ -600,8 +602,8 @@ if (!is_array($uids)) { $error = true; } - else { - $result = array_merge($result, $uids); + else if (!empty($uids)) { + $result = array_merge($result, $this->applyServerId($uids, $folder)); } break; } @@ -639,8 +641,8 @@ case self::RESULT_UID: $uids = $folder->get_uids($tag_filter); - if (is_array($uids)) { - $result = array_unique(array_merge($result, $uids)); + if (is_array($uids) && !empty($uids)) { + $result = array_unique(array_merge($result, $this->applyServerId($uids, $folder))); } break; @@ -749,7 +751,7 @@ $data[$relation['uid']] = array( 'name' => $relation['name'], 'changed' => $relation['changed']->format('U'), - 'members' => implode("\n", $relation['members']), + 'members' => implode("\n", (array)$relation['members']), ); } @@ -908,17 +910,6 @@ */ 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; @@ -953,10 +944,37 @@ $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); $folder = $this->getFolderObject($foldername); - if ($folder && $folder->valid && ($object = $folder->get_object($entryid))) { - $object['_folderid'] = $folderid; + if ($folder && $folder->valid) { + $crc = null; + $uid = $entryid; + + // See self::serverId() for full explanation + // Use (slower) UID prefix matching... + if (preg_match('/^CRC([0-9A-Fa-f]{8})(.+)$/', $uid, $matches)) { + $crc = $matches[1]; + $uid = $matches[2]; + + if (strlen($entryid) >= 64) { + foreach ($folder->select(array(array('uid', '~*', $uid))) as $object) { + if (($object['uid'] == $uid || strpos($object['uid'], $uid) === 0) + && $crc == $this->objectCRC($object['uid'], $folder) + ) { + $object['_folderid'] = $folderid; + return $object; + } + } + + continue; + } + } - return $object; + // Or (faster) strict UID matching... + if (($object = $folder->get_object($uid)) + && ($crc === null || $crc == $this->objectCRC($object['uid'], $folder)) + ) { + $object['_folderid'] = $folderid; + return $object; + } } } } @@ -990,6 +1008,8 @@ $this->setKolabTags($data['uid'], $tags); } + $data['_serverId'] = $this->serverId($data['uid'], $folder); + return $data; } } @@ -1015,6 +1035,8 @@ $this->setKolabTags($data['uid'], $tags); } + $data['_serverId'] = $this->serverId($object['uid'], $folder); + return $data; } } @@ -1030,7 +1052,7 @@ if ($object) { $folder = $this->getFolderObject($object['_mailbox']); - if ($folder && $folder->valid && $folder->delete($entryid)) { + if ($folder && $folder->valid && $folder->delete($object['uid'])) { if ($this->tag_categories) { $this->setKolabTags($object['uid'], null); } @@ -1506,7 +1528,7 @@ */ protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event') { - if (empty($data['recurrence'])) { + if (empty($data['recurrence']) || !empty($data['recurrence_date'])) { return; } @@ -1569,10 +1591,18 @@ $recurrence['monthOfYear'] = $month; } break; + + default: + return; + } + + // Skip all empty values (T2519) + if ($recurrence['type'] != self::RECUR_TYPE_DAILY) { + $recurrence = array_filter($recurrence); } // required field - $recurrence['interval'] = $r['INTERVAL'] ? $r['INTERVAL'] : 1; + $recurrence['interval'] = $r['INTERVAL'] ?: 1; if (!empty($r['UNTIL'])) { $recurrence['until'] = self::date_from_kolab($r['UNTIL']); @@ -1676,9 +1706,11 @@ // exceptions (modified occurences) foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) { $exception['_mailbox'] = $data['_mailbox']; - $ex = $this->getEntry($collection, $exception, true); - $ex['exceptionStartTime'] = clone $ex['startTime']; + $ex = $this->getEntry($collection, $exception, true); + $date = clone ($exception['recurrence_date'] ?: $ex['startTime']); + + $ex['exceptionStartTime'] = self::set_exception_time($date, $data['_start']); // remove fields not supported by Syncroton_Model_EventException unset($ex['uID']); @@ -1695,17 +1727,9 @@ continue; } - // set event start time to exception date - // that can't be any time, tested with Android - $hour = $data['_start']->format('H'); - $minute = $data['_start']->format('i'); - $second = $data['_start']->format('s'); - $exception->setTime($hour, $minute, $second); - $exception->_dateonly = false; - $ex = array( 'deleted' => 1, - 'exceptionStartTime' => self::date_from_kolab($exception), + 'exceptionStartTime' => self::set_exception_time($exception, $data['_start']), ); $ex_list[] = new Syncroton_Model_EventException($ex); @@ -1733,7 +1757,7 @@ $date->setTime(0, 0, 0); $rrule['EXDATE'][] = $date; } - else if (!$exception->deleted) { + else { $ex = $this->toKolab($exception, $folderid, null, $timezone); if ($data->allDayEvent) { @@ -1754,6 +1778,23 @@ } /** + * Sets ExceptionStartTime according to occurrence date and event start time + */ + protected static function set_exception_time($exception_date, $event_start) + { + if ($exception_date && $event_start) { + $hour = $event_start->format('H'); + $minute = $event_start->format('i'); + $second = $event_start->format('s'); + + $exception_date->setTime($hour, $minute, $second); + $exception_date->_dateonly = false; + + return self::date_from_kolab($exception_date); + } + } + + /** * Returns list of tag names assigned to kolab object */ protected function getKolabTags($uid, $categories = null) @@ -1819,4 +1860,84 @@ 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; + } + + /** + * Generate CRC-based ServerId from object UID + */ + protected function serverId($uid, $folder) + { + if ($this->modelName == 'mail') { + return $uid; + } + + // When ActiveSync communicates with the client, it refers to objects with a ServerId + // We can't use object UID for ServerId because: + // - ServerId is limited to 64 chars, + // - there can be multiple calendars with a copy of the same event. + // + // The solution is to; Take the original UID, and regardless of its length, execute the following: + // - Hash the UID concatenated with the Folder ID using CRC32b, + // - Prefix the UID with 'CRC' and the hash string, + // - Tryncate the result to 64 characters. + // + // Searching for the server-side copy of the object now follows the logic; + // - If the ServerId is prefixed with 'CRC', strip off the first 11 characters + // and we search for the UID using the remainder; + // - if the UID is shorter than 53 characters, it'll be the complete UID, + // - if the UID is longer than 53 characters, it'll be the truncated UID, + // and we search for a wildcard match of <uid>* + // When multiple copies of the same event are found, the same CRC32b hash can be used + // on the events metadata (i.e. the copy's UID and Folder ID), and compared with the CRC from the ServerId. + + // ServerId is max. 64 characters, below we generate a string of max. 64 chars + // Note: crc32b is always 8 characters + return 'CRC' . $this->objectCRC($uid, $folder) . substr($uid, 0, 53); + } + + /** + * Calculate checksum on object UID and folder UID + */ + protected function objectCRC($uid, $folder) + { + if (!is_object($folder)) { + $folder = $this->getFolderObject($folder); + } + + $folder_uid = $folder->get_uid(); + + return strtoupper(hash('crc32b', $folder_uid . $uid)); // always 8 chars + } + + /** + * Apply serverId() on a set of uids + */ + protected function applyServerId($uids, $folder) + { + if (!empty($uids) && $this->modelName != 'mail') { + $self = $this; + $func = function($uid) use ($self, $folder) { return $self->serverId($uid, $folder); }; + $uids = array_map($func, $uids); + } + + return $uids; + } }
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync_data_calendar.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync_data_calendar.php
Changed
@@ -308,7 +308,7 @@ } // Event meeting status - $result['meetingStatus'] = intval(!empty($result['attendees'])); + $this->meeting_status_from_kolab($collection, $event, $result); // Recurrence (and exceptions) $this->recurrence_from_kolab($collection, $event, $result); @@ -413,14 +413,6 @@ continue 2; } break; - - case 'uid': - // If UID is too long, use auto-generated UID (#1034) - // It's because UID is used as ServerId which cannot be longer than 64 chars - if (strlen($value) > 64) { - $value = null; - } - break; } $this->setKolabDataItem($event, $name, $value); @@ -543,6 +535,43 @@ } /** + * Set MeetingStatus according to event data + */ + protected function meeting_status_from_kolab($collection, $event, &$result) + { + // 0 - The event is an appointment, which has no attendees. + // 1 - The event is a meeting and the user is the meeting organizer. + // 3 - This event is a meeting, and the user is not the meeting organizer. + // 5 - The meeting has been canceled and the user was the meeting organizer. + // 7 - The meeting has been canceled. The user was not the meeting organizer. + $status = 0; + + if (!empty($event['attendees'])) { + // Find out if the user is an organizer + // TODO: Delegation/aliases support + $user_emails = kolab_sync::get_instance()->user->list_emails(); + $user_emails = array_map(function($v) { return $v['email']; }, $user_emails); + $is_organizer = true; + + foreach ($event['attendees'] as $attendee) { + if (in_array_nocase($attendee['email'], $user_emails)) { + $is_organizer = false; + break; + } + } + + if ($event['status'] == 'CANCELLED') { + $status = !empty($is_organizer) ? 5 : 7; + } + else { + $status = !empty($is_organizer) ? 1 : 3; + } + } + + $result['meetingStatus'] = $status; + } + + /** * Converts libkolab alarms spec. into a number of minutes */ protected function from_kolab_alarm($event)
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync_data_contacts.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync_data_contacts.php
Changed
@@ -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 @@ */ 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 @@ $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 @@ // 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 @@ } /** + * 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 @@ 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(); + } }
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync_data_email.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync_data_email.php
Changed
@@ -1311,7 +1311,17 @@ $body = rcube_enriched::to_html($body); } else { - $body = '<pre>' . $body . '</pre>'; + // Roundcube >= 1.2 + if (class_exists('rcube_text2html')) { + $flowed = $part->ctype_parameters['format'] == 'flowed'; + $delsp = $part->ctype_parameters['delsp'] == 'yes'; + $options = array('flowed' => $flowed, 'wrap' => false, 'delsp' => $delsp); + $text2html = new rcube_text2html($body, false, $options); + $body = '<html><body>' . $text2html->get_html() . '</body></html>'; + } + else { + $body = '<html><body><pre>' . $body . '</pre></body></html>'; + } } } else {
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync_data_gal.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync_data_gal.php
Changed
@@ -42,7 +42,7 @@ * * @var array */ - protected $address_books = array(); + protected static $address_books = array(); /** * Mapping from ActiveSync Contacts namespace fields @@ -193,7 +193,7 @@ // @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 @@ } foreach ($books as $idx => $book) { - $book = $this->get_address_book($idx); + $book = self::get_address_book($idx); if (!$book) { continue; @@ -284,14 +284,14 @@ * * @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 @@ $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 @@ * * @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 @@ 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;
View file
kolab-syncroton-2.3.5.tar.gz/lib/kolab_sync_timezone_converter.php -> kolab-syncroton-2.3.6.tar.gz/lib/kolab_sync_timezone_converter.php
Changed
@@ -1,17 +1,33 @@ <?php /** - * Tine 2.0 - * - * @package ActiveSync - * @license http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US) - * NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL), - * Version 1, the distribution of the Tine 2.0 ActiveSync module in or to the - * United States of America is excluded from the scope of this license. - * @copyright Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de) - * @author Jonas Fischer <j.fischer@metaways.de> - */ + +--------------------------------------------------------------------------+ + | Kolab Sync (ActiveSync for Kolab) | + | | + | Copyright (C) 2011-2017, Kolab Systems AG <contact@kolabsys.com> | + | Copyright (C) 2008-2012, Metaways Infosystems GmbH | + | | + | 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/> | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak <machniak@kolabsys.com> | + | Author: Jonas Fischer <j.fischer@metaways.de> | + +--------------------------------------------------------------------------+ +*/ +/** + * Activesync timezone converter + */ class kolab_sync_timezone_converter { /** @@ -52,7 +68,6 @@ /** * don't clone. Use the singleton. - * */ private function __clone() { @@ -73,7 +88,7 @@ } /** - * Returns an array of timezones that match to the {@param $_offsets} + * Returns a list of timezones that match to the {@param $_offsets} * * If {@see $_expectedTimezone} is set then the method will terminate as soon * as the expected timezone has matched and the expected timezone will be the @@ -114,12 +129,6 @@ } } -// $this->_log(__METHOD__, __LINE__, 'Matching timezones: '.print_r($timezones, true)); - -// if (empty($timezones)) { -// throw new ActiveSync_TimezoneNotFoundException('No timezone found for the given offsets'); -// } - return $timezones; } @@ -146,33 +155,12 @@ } /** - * Unpacks {@param $_packedTimezoneInfo} using {@see unpackTimezoneInfo} and then - * calls {@see getTimezoneForOffsets} with the unpacked timezone info + * Return packed string for given {@param $_timezone} * - * @param String $_packedTimezoneInfo - * @return String [timezone abbreviation e.g. CET, MST etc.] + * @param string $_timezone Timezone identifier + * @param string|int $_startDate Start date * - */ -// public function getTimezoneForPackedTimezoneInfo($_packedTimezoneInfo) -// { -// $offsets = $this->_unpackTimezoneInfo($_packedTimezoneInfo); -// $matchingTimezones = $this->getTimezoneForOffsets($offsets); -// $maxMatches = 0; -// $matchingAbbr = null; -// foreach ($matchingTimezones as $abbr => $timezones) { -// if (count($timezones) > $maxMatches) { -// $maxMatches = count($timezones); -// $matchingAbbr = $abbr; -// } -// } -// return $matchingAbbr; -// } - - /** - * Return packed string for given {@param $_timezone} - * @param String $_timezone - * @param String | int | null $_startDate - * @return String + * @return string Packed timezone offsets */ public function encodeTimezone($_timezone, $_startDate = null) { @@ -183,15 +171,17 @@ } $offsets = $this->getOffsetsForTimezone($_timezone, $_startDate); + return $this->_packTimezoneInfo($offsets); } /** - * get offsets for given timezone + * Get offsets for given timezone * - * @param string $_timezone - * @param $_startDate - * @return array + * @param string $_timezone Timezone identifier + * @param string|int $_startDate Start date + * + * @return array Timezone offsets */ public function getOffsetsForTimezone($_timezone, $_startDate = null) { @@ -206,7 +196,6 @@ $timezone = new DateTimeZone($_timezone); } catch (Exception $e) { -// $this->_log(__METHOD__, __LINE__, ": could not instantiate timezone {$_timezone}: {$e->getMessage()}"); return null; } @@ -217,13 +206,13 @@ if ($daylightTransition) { $offsets = $this->_generateOffsetsForTransition($offsets, $standardTransition, 'standard'); $offsets = $this->_generateOffsetsForTransition($offsets, $daylightTransition, 'daylight'); - $offsets['standardHour'] += $daylightTransition['offset']/3600; - $offsets['daylightHour'] += $standardTransition['offset']/3600; //@todo how do we get the standardBias (is usually 0)? //$offsets['standardBias'] = ... $offsets['daylightBias'] = ($daylightTransition['offset'] - $standardTransition['offset'])/60*-1; + $offsets['standardHour'] -= $offsets['daylightBias'] / 60; + $offsets['daylightHour'] += $offsets['daylightBias'] / 60; } } @@ -234,21 +223,26 @@ } /** + * Get offsets for timezone transition * + * @param array $_offsets Timezone offsets + * @param array $_transition Timezone transition information + * @param string $_type 'standard' or 'daylight' * - * @param array $_offsets - * @param array $_transition - * @param String $_type * @return array */ - protected function _generateOffsetsForTransition(Array $_offsets, Array $_transition, $_type) + protected function _generateOffsetsForTransition(array $_offsets, array $_transition, $_type) { - $transitionDateParsed = getdate($_transition['ts']); + $transitionDateParsed = new DateTime($_transition['time']); + + if ($_transition['offset']) { + $transitionDateParsed->modify($_transition['offset'] . ' seconds'); + } - $_offsets[$_type . 'Month'] = $transitionDateParsed['mon']; - $_offsets[$_type . 'DayOfWeek'] = $transitionDateParsed['wday']; - $_offsets[$_type . 'Minute'] = $transitionDateParsed['minutes']; - $_offsets[$_type . 'Hour'] = $transitionDateParsed['hours']; + $_offsets[$_type . 'Month'] = (int) $transitionDateParsed->format('n'); + $_offsets[$_type . 'DayOfWeek'] = (int) $transitionDateParsed->format('w'); + $_offsets[$_type . 'Minute'] = (int) $transitionDateParsed->format('i'); + $_offsets[$_type . 'Hour'] = (int) $transitionDateParsed->format('G'); for ($i=5; $i>0; $i--) { if ($this->_isNthOcurrenceOfWeekdayInMonth($_transition['ts'], $i)) { @@ -306,8 +300,8 @@ * Check if the given {@param $_standardTransition} and {@param $_daylightTransition} * match to the object property {@see $_offsets} * - * @param Array $standardTransition - * @param Array $daylightTransition + * @param array $standardTransition + * @param array $daylightTransition * * @return bool */ @@ -319,11 +313,11 @@ $standardOffset = ($_offsets['bias'] + $_offsets['standardBias']) * 60 * -1; - //check each condition in a single if statement and break the chain when one condition is not met - for performance reasons + // check each condition in a single if statement and break the chain when one condition is not met - for performance reasons if ($standardOffset == $_standardTransition['offset'] ) { if (empty($_offsets['daylightMonth']) && (empty($_daylightTransition) || empty($_daylightTransition['isdst']))) { - //No DST + // No DST return true; } @@ -336,7 +330,7 @@ $standardParsed = getdate($_standardTransition['ts']); $daylightParsed = getdate($_daylightTransition['ts']); - if ($standardParsed['mon'] == $_offsets['standardMonth'] && + if ($standardParsed['mon'] == $_offsets['standardMonth'] && $daylightParsed['mon'] == $_offsets['daylightMonth'] && $standardParsed['wday'] == $_offsets['standardDayOfWeek'] && $daylightParsed['wday'] == $_offsets['daylightDayOfWeek'] @@ -463,7 +457,7 @@ /** * Parse and set object property {@see $_startDate} * - * @param String | int $_startDate + * @param string|int $_startDate * @return void */ protected function _setStartDate($_startDate) @@ -529,10 +523,8 @@ protected function _checkTimezone(DateTimeZone $timezone, $offsets) { list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']); + if ($this->_checkTransition($standardTransition, $daylightTransition, $offsets)) { -// echo 'Matching timezone ' . $timezone->getName(); -// echo 'Matching daylight transition ' . print_r($daylightTransition, 1); -// echo 'Matching standard transition ' . print_r($standardTransition, 1); return $standardTransition; } @@ -544,25 +536,23 @@ * and {@param $_year}. * * @param DateTimeZone $_timezone - * @param $_year - * @return Array + * @param int $_year + * + * @return array */ protected function _getTransitionsForTimezoneAndYear(DateTimeZone $_timezone, $_year) { $standardTransition = null; $daylightTransition = null; - if (version_compare(PHP_VERSION, '5.3.0', '>=')) { - // Since php version 5.3.0 getTransitions accepts optional start and end parameters. - $start = mktime(0, 0, 0, 12, 1, $_year - 1); - $end = mktime(24, 0, 0, 12, 31, $_year); - $transitions = $_timezone->getTransitions($start, $end); - } else { - $transitions = $_timezone->getTransitions(); + $start = mktime(0, 0, 0, 12, 1, $_year - 1); + $end = mktime(24, 0, 0, 12, 31, $_year); + $transitions = $_timezone->getTransitions($start, $end); + + if ($transitions === false) { + return array(); } - $index = 0; //we need to access index counter outside of the foreach loop - $transition = array(); //we need to access the transition counter outside of the foreach loop foreach ($transitions as $index => $transition) { if (strftime('%Y', $transition['ts']) == $_year) { if (isset($transitions[$index+1]) && strftime('%Y', $transitions[$index]['ts']) == strftime('%Y', $transitions[$index+1]['ts'])) {
View file
kolab-syncroton-2.3.5.tar.gz/tests/timezone_converter.php -> kolab-syncroton-2.3.6.tar.gz/tests/timezone_converter.php
Changed
@@ -12,10 +12,80 @@ $converter = timezone_converter_test::getInstance(); $input = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAEAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAEAAAAAAAAAxP///w=='; - $output = $converter->getListOfTimezones($input, 'UTC'); + $output = $converter->getListOfTimezones($input); $this->assertTrue(is_array($output)); } + + function test_get_timezone() + { + $converter = timezone_converter_test::getInstance(); + $datetime = new DateTime('2017-01-01T12:00:00Z'); + + $offsets = $converter->getOffsetsForTimezone('UTC', $datetime); + $output = $converter->getTimezone($offsets, 'UTC'); + + $this->assertSame('UTC', $output); + + $offsets = $converter->getOffsetsForTimezone('Europe/Warsaw', $datetime); + $output = $converter->getTimezone($offsets, 'Europe/Warsaw'); + + $this->assertSame('Europe/Warsaw', $output); + + $offsets = $converter->getOffsetsForTimezone('America/Los_angeles', $datetime); + $output = $converter->getTimezone($offsets, 'America/Los_Angeles'); + + $this->assertSame('America/Los_Angeles', $output); + } + + function test_get_offsets_for_timezone() + { + $converter = timezone_converter_test::getInstance(); + $datetime = new DateTime('2017-01-01T12:00:00Z'); + + $output = $converter->getOffsetsForTimezone('UTC', $datetime); + + $this->assertSame($output['bias'], 0); + $this->assertSame($output['standardBias'], 0); + $this->assertSame($output['daylightBias'], 0); + $this->assertSame($output['standardMonth'], 0); + $this->assertSame($output['daylightMonth'], 0); + + $output = $converter->getOffsetsForTimezone('Europe/Warsaw', $datetime); + + $this->assertSame($output['standardBias'], 0); + $this->assertSame($output['standardMonth'], 10); + $this->assertSame($output['standardDay'], 5); + $this->assertSame($output['standardHour'], 3); + $this->assertSame($output['daylightBias'], -60); + $this->assertSame($output['daylightMonth'], 3); + $this->assertSame($output['daylightDay'], 5); + $this->assertSame($output['daylightHour'], 2); + + $output = $converter->getOffsetsForTimezone('America/Los_Angeles', $datetime); + + $this->assertSame($output['bias'], 480); + $this->assertSame($output['standardBias'], 0); + $this->assertSame($output['standardMonth'], 11); + $this->assertSame($output['standardDay'], 1); + $this->assertSame($output['standardHour'], 2); + $this->assertSame($output['daylightBias'], -60); + $this->assertSame($output['daylightMonth'], 3); + $this->assertSame($output['daylightDay'], 2); + $this->assertSame($output['daylightHour'], 2); + + $output = $converter->getOffsetsForTimezone('Atlantic/Azores', $datetime); + + $this->assertSame($output['bias'], 60); + $this->assertSame($output['standardBias'], 0); + $this->assertSame($output['standardMonth'], 10); + $this->assertSame($output['standardDay'], 5); + $this->assertSame($output['standardHour'], 1); + $this->assertSame($output['daylightBias'], -60); + $this->assertSame($output['daylightMonth'], 3); + $this->assertSame($output['daylightDay'], 5); + $this->assertSame($output['daylightHour'], 0); + } } class timezone_converter_test extends kolab_sync_timezone_converter
View file
kolab-syncroton.dsc
Changed
@@ -2,7 +2,7 @@ Source: kolab-syncroton Binary: kolab-syncroton Architecture: all -Version: 2.3.5-0~kolab4 +Version: 2.3.6-0~kolab1 Maintainer: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> Uploaders: Paul Klos <kolab@klos2day.nl> Homepage: http://www.kolab.org/ @@ -12,5 +12,5 @@ Package-List: kolab-syncroton deb utils extra Files: - 00000000000000000000000000000000 0 kolab-syncroton-2.3.5.tar.gz + 00000000000000000000000000000000 0 kolab-syncroton-2.3.6.tar.gz 00000000000000000000000000000000 0 debian.tar.gz
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
.