Projects
Kolab:16:Enterprise
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 ($option0 == '/') { -+ 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 ($f0 == '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($emails0); -+ } -+ -+ 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))); -+ $items0 = rcube_ldap::dn_decode($items0); -+ 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 = $matches1; + $uid = $matches2; + + 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 ($option0 == '/') { + 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 ($f0 == '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($emails0); + } + + 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))); + $items0 = rcube_ldap::dn_decode($items0); + 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
.