Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1841936
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
63 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php
index 91a4362..6ee7eb0 100644
--- a/lib/kolab_sync_data.php
+++ b/lib/kolab_sync_data.php
@@ -1,1995 +1,2003 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, 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 |
| 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> |
+--------------------------------------------------------------------------+
*/
/**
* Base class for Syncroton data backends
*/
abstract class kolab_sync_data implements Syncroton_Data_IData
{
/**
* ActiveSync protocol version
*
* @var int
*/
protected $asversion = 0;
/**
* information about the current device
*
* @var Syncroton_Model_IDevice
*/
protected $device;
/**
* timestamp to use for all sync requests
*
* @var DateTime
*/
protected $syncTimeStamp;
/**
* name of model to use
*
* @var string
*/
protected $modelName;
/**
* type of the default folder
*
* @var int
*/
protected $defaultFolderType;
/**
* default container for new entries
*
* @var string
*/
protected $defaultFolder;
/**
* type of user created folders
*
* @var int
*/
protected $folderType;
/**
* Internal cache for kolab_storage folder objects
*
* @var array
*/
protected $folders = array();
/**
* Internal cache for IMAP folders list
*
* @var array
*/
protected $imap_folders = array();
/**
* Logger instance.
*
* @var kolab_sync_logger
*/
protected $logger;
/**
* Timezone
*
* @var string
*/
protected $timezone;
/**
* List of device types with multiple folders support
*
* @var array
*/
protected $ext_devices = array(
'iphone',
'ipad',
'thundertine',
'windowsphone',
'wp',
'wp8',
'playbook',
);
const RESULT_OBJECT = 0;
const RESULT_UID = 1;
const RESULT_COUNT = 2;
/**
* Recurrence types
*/
const RECUR_TYPE_DAILY = 0; // Recurs daily.
const RECUR_TYPE_WEEKLY = 1; // Recurs weekly
const RECUR_TYPE_MONTHLY = 2; // Recurs monthly
const RECUR_TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day
const RECUR_TYPE_YEARLY = 5; // Recurs yearly
const RECUR_TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day
/**
* Day of week constants
*/
const RECUR_DOW_SUNDAY = 1;
const RECUR_DOW_MONDAY = 2;
const RECUR_DOW_TUESDAY = 4;
const RECUR_DOW_WEDNESDAY = 8;
const RECUR_DOW_THURSDAY = 16;
const RECUR_DOW_FRIDAY = 32;
const RECUR_DOW_SATURDAY = 64;
const RECUR_DOW_LAST = 127; // The last day of the month. Used as a special value in monthly or yearly recurrences.
/**
* Mapping of recurrence types
*
* @var array
*/
protected $recurTypeMap = array(
self::RECUR_TYPE_DAILY => 'DAILY',
self::RECUR_TYPE_WEEKLY => 'WEEKLY',
self::RECUR_TYPE_MONTHLY => 'MONTHLY',
self::RECUR_TYPE_MONTHLY_DAYN => 'MONTHLY',
self::RECUR_TYPE_YEARLY => 'YEARLY',
self::RECUR_TYPE_YEARLY_DAYN => 'YEARLY',
);
/**
* Mapping of weekdays
* NOTE: ActiveSync uses a bitmask
*
* @var array
*/
protected $recurDayMap = array(
'SU' => self::RECUR_DOW_SUNDAY,
'MO' => self::RECUR_DOW_MONDAY,
'TU' => self::RECUR_DOW_TUESDAY,
'WE' => self::RECUR_DOW_WEDNESDAY,
'TH' => self::RECUR_DOW_THURSDAY,
'FR' => self::RECUR_DOW_FRIDAY,
'SA' => self::RECUR_DOW_SATURDAY,
);
/**
* the constructor
*
* @param Syncroton_Model_IDevice $device
* @param DateTime $syncTimeStamp
*/
public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp)
{
$this->backend = kolab_sync_backend::get_instance();
$this->device = $device;
$this->asversion = floatval($device->acsversion);
$this->syncTimeStamp = $syncTimeStamp;
$this->logger = Syncroton_Registry::get(Syncroton_Registry::LOGGERBACKEND);
$this->defaultRootFolder = $this->defaultFolder . '::Syncroton';
// set internal timezone of kolab_format to user timezone
try {
$this->timezone = rcube::get_instance()->config->get('timezone', 'GMT');
kolab_format::$timezone = new DateTimeZone($this->timezone);
}
catch (Exception $e) {
//rcube::raise_error($e, true);
$this->timezone = 'GMT';
kolab_format::$timezone = new DateTimeZone('GMT');
}
}
/**
* return list of supported folders for this backend
*
* @return array
*/
public function getAllFolders()
{
$list = array();
// device supports multiple folders ?
if ($this->isMultiFolder()) {
// get the folders the user has access to
$list = $this->listFolders();
}
else if ($default = $this->getDefaultFolder()) {
$list = array($default['serverId'] => $default);
}
// getAllFolders() is called only in FolderSync
// throw Syncroton_Exception_Status_FolderSync exception
if (!is_array($list)) {
throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR);
}
foreach ($list as $idx => $folder) {
$list[$idx] = new Syncroton_Model_Folder($folder);
}
return $list;
}
/**
* Retrieve folders which were modified since last sync
*
* @param DateTime $startTimeStamp
* @param DateTime $endTimeStamp
*
* @return array List of folders
*/
public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp)
{
return array();
}
/**
* Returns true if the device supports multiple folders or it was configured so
*/
protected function isMultiFolder()
{
$config = rcube::get_instance()->config;
$blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName);
if (!is_array($blacklist)) {
$blacklist = $config->get('activesync_multifolder_blacklist');
}
if (is_array($blacklist)) {
return !$this->deviceTypeFilter($blacklist);
}
return in_array_nocase($this->device->devicetype, $this->ext_devices);
}
/**
* Returns default folder for current class type.
*/
protected function getDefaultFolder()
{
// Check if there's any folder configured for sync
$folders = $this->listFolders();
if (empty($folders)) {
return $folders;
}
foreach ($folders as $folder) {
if ($folder['type'] == $this->defaultFolderType) {
$default = $folder;
break;
}
}
// Return first on the list if there's no default
if (empty($default)) {
$key = array_shift(array_keys($folders));
$default = $folders[$key];
// make sure the type is default here
$default['type'] = $this->defaultFolderType;
}
// Remember real folder ID and set ID/name to root folder
$default['realid'] = $default['serverId'];
$default['serverId'] = $this->defaultRootFolder;
$default['displayName'] = $this->defaultFolder;
return $default;
}
/**
* Creates a folder
*/
public function createFolder(Syncroton_Model_IFolder $folder)
{
$parentid = $folder->parentId;
$type = $folder->type;
$display_name = $folder->displayName;
if ($parentid) {
$parent = $this->backend->folder_id2name($parentid, $this->device->deviceid);
if ($parent === null) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND);
}
}
$name = rcube_charset::convert($display_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parent !== null) {
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
// Create IMAP folder
$result = $this->backend->folder_create($name, $type, $this->device->deviceid);
if ($result) {
$folder->serverId = $this->backend->folder_id($name);
return $folder;
}
$errno = Syncroton_Exception_Status_FolderCreate::UNKNOWN_ERROR;
// Special case when client tries to create a subfolder of INBOX
// which is not possible on Cyrus-IMAP (T2223)
if ($parent == 'INBOX' && stripos($this->backend->last_error(), 'invalid') !== false) {
$errno = Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER;
}
// Note: Looks like Outlook 2013 ignores any errors on FolderCreate command
throw new Syncroton_Exception_Status_FolderCreate($errno);
}
/**
* Updates a folder
*/
public function updateFolder(Syncroton_Model_IFolder $folder)
{
$parentid = $folder->parentId;
$type = $folder->type;
$display_name = $folder->displayName;
$old_name = $this->backend->folder_id2name($folder->serverId, $this->device->deviceid);
if ($parentid) {
$parent = $this->backend->folder_id2name($parentid, $this->device->deviceid);
}
$name = rcube_charset::convert($display_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parent !== null) {
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
// Rename/move IMAP folder
if ($name == $old_name) {
$result = true;
// @TODO: folder type change?
}
else {
$result = $this->backend->folder_rename($old_name, $name, $type);
}
if ($result) {
return $folder;
}
// @TODO: throw exception
}
/**
* Deletes a folder
*/
public function deleteFolder($folder)
{
if ($folder instanceof Syncroton_Model_IFolder) {
$folder = $folder->serverId;
}
$name = $this->backend->folder_id2name($folder, $this->device->deviceid);
// @TODO: throw exception
return $this->backend->folder_delete($name, $this->device->deviceid);
}
/**
* Empty folder (remove all entries and optionally subfolders)
*
* @param string $folderId Folder identifier
* @param array $options Options
*/
public function emptyFolderContents($folderid, $options)
{
$folders = $this->extractFolders($folderid);
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
// Remove all entries
$folder->delete_all();
// Remove subfolders
if (!empty($options['deleteSubFolders'])) {
$list = $this->listFolders($folderid);
foreach ($list as $folderid => $folder) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
// Remove all entries
$folder->delete_all();
}
}
}
}
/**
* 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)
{
$item = $this->getObject($srcFolderId, $serverId, $folder);
if (!$item || !$folder) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
$dstname = $this->backend->folder_id2name($dstFolderId, $this->device->deviceid);
if ($dstname === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if (!$folder->move($serverId, $dstname)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
return $item['uid'];
}
/**
* 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)
{
$entry = $this->toKolab($entry, $folderId);
$entry = $this->createObject($folderId, $entry);
if (empty($entry)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $entry['_serverId'];
}
/**
* 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)
{
$oldEntry = $this->getObject($folderId, $serverId);
if (empty($oldEntry)) {
throw new Syncroton_Exception_NotFound('id not found');
}
$entry = $this->toKolab($entry, $folderId, $oldEntry);
$entry = $this->updateObject($folderId, $serverId, $entry);
if (empty($entry)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $entry['_serverId'];
}
/**
* delete entry
*
* @param string $folderId
* @param string $serverId
* @param array $collectionData
*/
public function deleteEntry($folderId, $serverId, $collectionData)
{
$deleted = $this->deleteObject($folderId, $serverId);
if (!$deleted) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
}
public function getFileReference($fileReference)
{
// to be implemented by Email data class
// @TODO: throw "unimplemented" exception here?
}
/**
* 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)
{
if ($folderid == $this->defaultRootFolder) {
$folders = $this->listFolders();
if (!is_array($folders)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$folders = array_keys($folders);
}
else {
$folders = array($folderid);
}
// there's a PHP Warning from kolab_storage if $filter isn't an array
if (empty($filter)) {
$filter = array();
}
else {
$changed_objects = $this->getChangesByRelations($folderid, $filter);
}
$result = $result_type == self::RESULT_COUNT ? 0 : array();
$found = 0;
foreach ($folders as $folder_id) {
$foldername = $this->backend->folder_id2name($folder_id, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$found++;
$error = false;
switch ($result_type) {
case self::RESULT_COUNT:
$count = $folder->count($filter);
if ($count === null || $count === false) {
$error = true;
}
else {
$result += (int) $count;
}
break;
case self::RESULT_UID:
$uids = $folder->get_uids($filter);
if (!is_array($uids)) {
$error = true;
}
else if (!empty($uids)) {
$result = array_merge($result, $this->applyServerId($uids, $folder));
}
break;
}
if ($error) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
// handle tag modifications
if (!empty($changed_objects)) {
// build new filter
// search objects mathing current filter,
// relations may contain members of many types, we need to
// search them by UID in all requested folders to get
// only these with requested type (and that really exist
// in specified folders)
$tag_filter = array(array('uid', '=', $changed_objects));
foreach ($filter as $f) {
if ($f[0] != 'changed') {
$tag_filter[] = $f;
}
}
switch ($result_type) {
case self::RESULT_COUNT:
// Note: this way we're potentally counting the same objects twice
// I'm not sure if this is a problem, we most likely do not
// need a precise result here
$count = $folder->count($tag_filter);
if ($count !== null && $count !== false) {
$result += (int) $count;
}
break;
case self::RESULT_UID:
$uids = $folder->get_uids($tag_filter);
if (is_array($uids) && !empty($uids)) {
$result = array_unique(array_merge($result, $this->applyServerId($uids, $folder)));
}
break;
}
}
}
if (!$found) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
return $result;
}
/**
* Detect changes of relation (tag) objects data and assigned objects
* Returns relation member identifiers
*/
protected function getChangesByRelations($folderid, $filter)
{
if (!$this->tag_categories) {
return;
}
// get period filter, create new objects filter
foreach ($filter as $f) {
if ($f[0] == 'changed' && $f[1] == '>') {
$since = $f[2];
}
}
// this is not search for changes, do nothing
if (empty($since)) {
return;
}
// get relations state from the last sync
$last_state = (array) $this->backend->relations_state_get($this->device->id, $folderid, $since);
// get current relations state
$config = kolab_storage_config::get_instance();
$default = true;
$filter = array(
array('type', '=', 'relation'),
array('category', '=', 'tag')
);
$relations = $config->get_objects($filter, $default, 100);
$result = array();
$changed = false;
// compare states, get members of changed relations
foreach ($relations as $relation) {
$rel_id = $relation['uid'];
if ($relation['changed']) {
$relation['changed']->setTimezone(new DateTimeZone('UTC'));
}
// last state unknown...
if (empty($last_state[$rel_id])) {
// ...get all members
if (!empty($relation['members'])) {
$changed = true;
$result = array_merge($result, $relation['members']);
}
}
// last state known, changed tag name...
else if ($last_state[$rel_id]['name'] != $relation['name']) {
// ...get all (old and new) members
$members_old = explode("\n", $last_state[$rel_id]['members']);
$changed = true;
$members = array_unique(array_merge($relation['members'], $members_old));
$result = array_merge($result, $members);
}
// last state known, any other change change...
else if ($last_state[$rel_id]['changed'] < $relation['changed']->format('U')) {
// ...find new and removed members
$members_old = explode("\n", $last_state[$rel_id]['members']);
$new = array_diff($relation['members'], $members_old);
$removed = array_diff($members_old, $relation['members']);
if (!empty($new) || !empty($removed)) {
$changed = true;
$result = array_merge($result, $new, $removed);
}
}
unset($last_state[$rel_id]);
}
// get members of deleted relations
if (!empty($last_state)) {
$changed = true;
foreach ($last_state as $relation) {
$members = explode("\n", $relation['members']);
$result = array_merge($result, $members);
}
}
// save current state
if ($changed) {
$data = array();
foreach ($relations as $relation) {
$data[$relation['uid']] = array(
'name' => $relation['name'],
'changed' => $relation['changed']->format('U'),
'members' => implode("\n", (array)$relation['members']),
);
}
$now = new DateTime('now', new DateTimeZone('UTC'));
$this->backend->relations_state_set($this->device->id, $folderid, $now, $data);
}
// in mail mode return only message URIs
if ($this->modelName == 'mail') {
// lambda function to skip email members
$filter_func = function($value) {
return strpos($value, 'imap://') === 0;
};
$result = array_filter(array_unique($result), $filter_func);
}
// otherwise return only object UIDs
else {
// lambda function to skip email members
$filter_func = function($value) {
return strpos($value, 'urn:uuid:') === 0;
};
// lambda function to parse member URI
$member_func = function($value) {
if (strpos($value, 'urn:uuid:') === 0) {
$value = substr($value, 9);
}
return $value;
};
$result = array_map($member_func, array_filter(array_unique($result), $filter_func));
}
return $result;
}
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
// overwrite by child class according to specified type
return array();
}
/**
* get all entries changed between two dates
*
* @param string $folderId
* @param DateTime $start
* @param DateTime $end
* @param int $filterType
*
* @return array
*/
public function getChangedEntries($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
{
$filter = $this->filter($filter_type);
$filter[] = array('changed', '>', $start);
if ($end) {
$filter[] = array('changed', '<=', $end);
}
return $this->searchEntries($folderId, $filter, self::RESULT_UID);
}
/**
* Get count of entries changed between two dates
*
* @param string $folderId
* @param DateTime $start
* @param DateTime $end
* @param int $filterType
*
* @return int
*/
public function getChangedEntriesCount($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
{
$filter = $this->filter($filter_type);
$filter[] = array('changed', '>', $start);
if ($end) {
$filter[] = array('changed', '<=', $end);
}
return $this->searchEntries($folderId, $filter, self::RESULT_COUNT);
}
/**
* get id's of all entries available on the server
*
* @param string $folderId
* @param int $filterType
*
* @return array
*/
public function getServerEntries($folder_id, $filter_type)
{
$filter = $this->filter($filter_type);
$result = $this->searchEntries($folder_id, $filter, self::RESULT_UID);
return $result;
}
/**
* get count of all entries available on the server
*
* @param string $folderId
* @param int $filterType
*
* @return int
*/
public function getServerEntriesCount($folder_id, $filter_type)
{
$filter = $this->filter($filter_type);
$result = $this->searchEntries($folder_id, $filter, self::RESULT_COUNT);
return $result;
}
/**
* Returns number of changed objects in the backend folder
*
* @param Syncroton_Backend_IContent $contentBackend
* @param Syncroton_Model_IFolder $folder
* @param Syncroton_Model_ISyncState $syncState
*
* @return int
*/
public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
$allClientEntries = $contentBackend->getFolderState($this->device, $folder);
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$changedEntries = $this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
return count($addedEntries) + count($deletedEntries) + $changedEntries;
}
/**
* Returns true if any data got modified in the backend folder
*
* @param Syncroton_Backend_IContent $contentBackend
* @param Syncroton_Model_IFolder $folder
* @param Syncroton_Model_ISyncState $syncState
*
* @return bool
*/
public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
try {
if ($this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype)) {
return true;
}
$allClientEntries = $contentBackend->getFolderState($this->device, $folder);
// @TODO: Consider looping over all folders here, not in getServerEntries() and
// getChangedEntriesCount(). This way we could break the loop and not check all folders
// or at least skip redundant cache sync of the same folder
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
return count($addedEntries) > 0 || count($deletedEntries) > 0;
}
catch (Exception $e) {
// return "no changes" if something failed
return false;
}
}
/**
* Fetches the entry from the backend
*/
protected function getObject($folderid, $entryid, &$folder = null)
{
$folders = $this->extractFolders($folderid);
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
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;
}
}
// Or (faster) strict UID matching...
if (($object = $folder->get_object($uid))
&& ($crc === null || $crc == $this->objectCRC($object['uid'], $folder))
) {
$object['_folderid'] = $folderid;
return $object;
}
}
}
}
/**
* Saves the entry on the backend
*/
protected function createObject($folderid, $data)
{
if ($folderid == $this->defaultRootFolder) {
$default = $this->getDefaultFolder();
if (!is_array($default)) {
return null;
}
$folderid = isset($default['realid']) ? $default['realid'] : $default['serverId'];
}
// convert categories into tags, save them after creating an object
if ($this->tag_categories) {
$tags = $data['categories'];
unset($data['categories']);
}
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
+ // Set User-Agent for saved objects
+ $app = kolab_sync::get_instance();
+ $app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
+
if ($folder && $folder->valid && $folder->save($data)) {
if (!empty($tags)) {
$this->setKolabTags($data['uid'], $tags);
}
$data['_serverId'] = $this->serverId($data['uid'], $folder);
return $data;
}
}
/**
* Updates the entry on the backend
*/
protected function updateObject($folderid, $entryid, $data)
{
$object = $this->getObject($folderid, $entryid);
if ($object) {
$folder = $this->getFolderObject($object['_mailbox']);
// convert categories into tags, save them after updating an object
if ($this->tag_categories && array_key_exists('categories', $data)) {
$tags = (array) $data['categories'];
unset($data['categories']);
}
+ // Set User-Agent for saved objects
+ $app = kolab_sync::get_instance();
+ $app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
+
if ($folder && $folder->valid && $folder->save($data)) {
if (isset($tags)) {
$this->setKolabTags($data['uid'], $tags);
}
$data['_serverId'] = $this->serverId($object['uid'], $folder);
return $data;
}
}
}
/**
* Removes the entry from the backend
*/
protected function deleteObject($folderid, $entryid)
{
$object = $this->getObject($folderid, $entryid);
if ($object) {
$folder = $this->getFolderObject($object['_mailbox']);
if ($folder && $folder->valid && $folder->delete($object['uid'])) {
if ($this->tag_categories) {
$this->setKolabTags($object['uid'], null);
}
return true;
}
return false;
}
// object doesn't exist, confirm deletion
return true;
}
/**
* Returns internal folder IDs
*
* @param string $folderid Folder identifier
*
* @return array List of folder identifiers
*/
protected function extractFolders($folderid)
{
if ($folderid instanceof Syncroton_Model_IFolder) {
$folderid = $folderid->serverId;
}
if ($folderid == $this->defaultRootFolder) {
$folders = $this->listFolders();
if (!is_array($folders)) {
return null;
}
$folders = array_keys($folders);
}
else {
$folders = array($folderid);
}
return $folders;
}
/**
* List of all IMAP folders (or subtree)
*
* @param string $parentid Parent folder identifier
*
* @return array List of folder identifiers
*/
protected function listFolders($parentid = null)
{
if (empty($this->imap_folders)) {
$this->imap_folders = $this->backend->folders_list(
$this->device->deviceid, $this->modelName, $this->isMultiFolder());
}
if ($parentid === null) {
return $this->imap_folders;
}
$folders = array();
$parents = array($parentid);
foreach ($this->imap_folders as $folder_id => $folder) {
if ($folder['parentId'] && in_array($folder['parentId'], $parents)) {
$folders[$folder_id] = $folder;
$parents[] = $folder_id;
}
}
return $folders;
}
/**
* Returns Folder object (uses internal cache)
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return kolab_storage_folder Folder object
*/
protected function getFolderObject($name)
{
if ($name === null || $name === '') {
return null;
}
if (!isset($this->folders[$name])) {
$this->folders[$name] = kolab_storage::get_folder($name, $this->modelName);
}
return $this->folders[$name];
}
/**
* Returns ActiveSync settings of specified folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return array Folder settings
*/
protected function getFolderConfig($name)
{
$metadata = $this->backend->folder_meta();
if (!is_array($metadata)) {
return array();
}
$deviceid = $this->device->deviceid;
$config = $metadata[$name]['FOLDER'][$deviceid];
return array(
'ALARMS' => $config['S'] == 2,
);
}
/**
* Returns real folder name for specified folder ID
*/
protected function getFolderName($folderid)
{
if ($folderid == $this->defaultRootFolder) {
$default = $this->getDefaultFolder();
if (!is_array($default)) {
return null;
}
$folderid = isset($default['realid']) ? $default['realid'] : $default['serverId'];
}
return $this->backend->folder_id2name($folderid, $this->device->deviceid);
}
/**
* Returns folder ID from Kolab folder object
*/
protected function getFolderId($folder)
{
if (!$this->isMultiFolder()) {
return $this->defaultRootFolder;
}
return $this->backend->folder_id($folder->get_name(), $folder->get_type());
}
/**
* Convert contact from xml to kolab format
*
* @param Syncroton_Model_IEntry $data Contact data
* @param string $folderId Folder identifier
* @param array $entry Old Contact data for merge
*
* @return array
*/
abstract function toKolab(Syncroton_Model_IEntry $data, $folderId, $entry = null);
/**
* Extracts data from kolab data array
*/
protected function getKolabDataItem($data, $name)
{
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!empty($data[$name]) && is_array($data[$name])) {
foreach ($data[$name] as $element) {
if ($element['type'] == $type) {
return $element[$key_name];
}
}
}
return null;
}
// custom properties
if ($count == 2 && $name_items[0] == 'x-custom') {
$value = null;
foreach ((array) $data['x-custom'] as $val) {
if (is_array($val) && $val[0] == $name_items[1]) {
$value = $val[1];
break;
}
}
return $value;
}
$name_items = explode(':', $name);
$name = $name_items[0];
if (empty($data[$name])) {
return null;
}
// simple array (e.g. email)
if (count($name_items) == 2) {
return $data[$name][$name_items[1]];
}
return $data[$name];
}
/**
* Saves data in kolab data array
*/
protected function setKolabDataItem(&$data, $name, $value)
{
if (empty($value)) {
return $this->unsetKolabDataItem($data, $name);
}
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!isset($data[$name])) {
$data[$name] = array();
}
foreach ($data[$name] as $idx => $element) {
if ($element['type'] == $type) {
$found = $idx;
break;
}
}
if (!isset($found)) {
$data[$name] = array_values($data[$name]);
$found = count($data[$name]);
$data[$name][$found] = array('type' => $type);
}
$data[$name][$found][$key_name] = $value;
return;
}
// custom properties
if ($count == 2 && $name_items[0] == 'x-custom') {
foreach ((array) $data['x-custom'] as $idx => $val) {
if (is_array($val) && $val[0] == $name_items[1]) {
$data['x-custom'][$idx][1] = $value;
return;
}
}
$data['x-custom'][] = array($name_items[1], $value);
return;
}
$name_items = explode(':', $name);
$name = $name_items[0];
// simple array (e.g. email)
if (count($name_items) == 2) {
$data[$name][$name_items[1]] = $value;
return;
}
$data[$name] = $value;
}
/**
* Unsets data item in kolab data array
*/
protected function unsetKolabDataItem(&$data, $name)
{
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!isset($data[$name])) {
return;
}
foreach ($data[$name] as $idx => $element) {
if ($element['type'] == $type) {
$found = $idx;
break;
}
}
if (!isset($found)) {
return;
}
unset($data[$name][$found][$key_name]);
// if there's only one element and it's 'type', remove it
if (count($data[$name][$found]) == 1 && isset($data[$name][$found]['type'])) {
unset($data[$name][$found]['type']);
}
if (empty($data[$name][$found])) {
unset($data[$name][$found]);
}
if (empty($data[$name])) {
unset($data[$name]);
}
return;
}
// custom properties
if ($count == 2 && $name_items[0] == 'x-custom') {
foreach ((array) $data['x-custom'] as $idx => $val) {
if (is_array($val) && $val[0] == $name_items[1]) {
unset($data['x-custom'][$idx]);
}
}
}
$name_items = explode(':', $name);
$name = $name_items[0];
// simple array (e.g. email)
if (count($name_items) == 2) {
unset($data[$name][$name_items[1]]);
if (empty($data[$name])) {
unset($data[$name]);
}
return;
}
unset($data[$name]);
}
/**
* Setter for Body attribute according to client version
*
* @param string $value Body
* @param array $param Body parameters
*
* @reurn Syncroton_Model_EmailBody Body element
*/
protected function setBody($value, $params = array())
{
if (empty($value) && empty($params)) {
return;
}
// Old protocol version doesn't support AirSyncBase:Body, it's eg. WindowsCE
if ($this->asversion < 12) {
return;
}
if (!empty($value)) {
// cast to string to workaround issue described in Bug #1635
$params['data'] = (string) $value;
}
if (!isset($params['type'])) {
$params['type'] = Syncroton_Model_EmailBody::TYPE_PLAINTEXT;
}
return new Syncroton_Model_EmailBody($params);
}
/**
* Getter for Body attribute value according to client version
*
* @param mixed $body Body element
* @param int $type Result data type (to which the body will be converted, if specified).
* One or array of Syncroton_Model_EmailBody constants.
*
* @return string Body value
*/
protected function getBody($body, $type = null)
{
if ($body && $body->data) {
$data = $body->data;
}
if (!$data || empty($type)) {
return;
}
$type = (array) $type;
// Convert to specified type
if (!in_array($body->type, $type)) {
$converter = new kolab_sync_body_converter($data, $body->type);
$data = $converter->convert($type[0]);
}
return $data;
}
/**
* Converts text (plain or html) into ActiveSync Body element.
* Takes bodyPreferences into account and detects if the text is plain or html.
*/
protected function body_from_kolab($body, $collection)
{
if (empty($body)) {
return;
}
$opts = $collection->options;
$prefs = $opts['bodyPreferences'];
$html_type = Syncroton_Command_Sync::BODY_TYPE_HTML;
$type = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
$params = array();
// HTML? check for opening and closing <html> or <body> tags
$is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, '</'.$m[1].'>') > 0;
// here we assume that all devices support plain text
if ($is_html) {
// device supports HTML...
if (!empty($prefs[$html_type])) {
$type = $html_type;
}
// ...else convert to plain text
else {
$txt = new rcube_html2text($body, false, true);
$body = $txt->get_text();
}
}
// strip out any non utf-8 characters
$body = rcube_charset::clean($body);
$real_length = $body_length = strlen($body);
// truncate the body if needed
if (($truncateAt = $prefs[$type]['truncationSize']) && $body_length > $truncateAt) {
$body = mb_strcut($body, 0, $truncateAt);
$body_length = strlen($body);
$params['truncated'] = 1;
$params['estimatedDataSize'] = $real_length;
}
$params['type'] = $type;
return $this->setBody($body, $params);
}
/**
* Converts PHP DateTime, date (YYYY-MM-DD) or unixtimestamp into PHP DateTime in UTC
*
* @param DateTime|int|string $date Unix timestamp, date (YYYY-MM-DD) or PHP DateTime object
*
* @return DateTime Datetime object
*/
protected static function date_from_kolab($date)
{
if (!empty($date)) {
if (is_numeric($date)) {
$date = new DateTime('@' . $date);
}
else if (is_string($date)) {
$date = new DateTime($date, new DateTimeZone('UTC'));
}
else if ($date instanceof DateTime) {
$date = clone $date;
$tz = $date->getTimezone();
$tz_name = $tz->getName();
// convert to UTC if needed
if ($tz_name != 'UTC') {
$utc = new DateTimeZone('UTC');
// safe dateonly object conversion to UTC
// note: _dateonly flag is set by libkolab e.g. for birthdays
if ($date->_dateonly) {
// avoid time change
$date = new DateTime($date->format('Y-m-d'), $utc);
// set time to noon to avoid timezone troubles
$date->setTime(12, 0, 0);
}
else {
$date->setTimezone($utc);
}
}
}
else {
return null; // invalid input
}
return $date;
}
}
/**
* Convert Kolab event/task recurrence into ActiveSync
*/
protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event')
{
if (empty($data['recurrence']) || !empty($data['recurrence_date'])) {
return;
}
$recurrence = array();
$r = $data['recurrence'];
// required fields
switch($r['FREQ']) {
case 'DAILY':
$recurrence['type'] = self::RECUR_TYPE_DAILY;
break;
case 'WEEKLY':
$recurrence['type'] = self::RECUR_TYPE_WEEKLY;
$recurrence['dayOfWeek'] = $this->day2bitmask($r['BYDAY']);
break;
case 'MONTHLY':
if (!empty($r['BYMONTHDAY'])) {
// @TODO: ActiveSync doesn't support multi-valued month days,
// should we replicate the recurrence element for each day of month?
$month_day = array_shift(explode(',', $r['BYMONTHDAY']));
$recurrence['type'] = self::RECUR_TYPE_MONTHLY;
$recurrence['dayOfMonth'] = $month_day;
}
else {
$week = (int) substr($r['BYDAY'], 0, -2);
$week = ($week == -1) ? 5 : $week;
$day = substr($r['BYDAY'], -2);
$recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN;
$recurrence['weekOfMonth'] = $week;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
}
break;
case 'YEARLY':
// @TODO: ActiveSync doesn't support multi-valued months,
// should we replicate the recurrence element for each month?
$month = array_shift(explode(',', $r['BYMONTH']));
if (!empty($r['BYDAY'])) {
$week = (int) substr($r['BYDAY'], 0, -2);
$week = ($week == -1) ? 5 : $week;
$day = substr($r['BYDAY'], -2);
$recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN;
$recurrence['weekOfMonth'] = $week;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
$recurrence['monthOfYear'] = $month;
}
else if (!empty($r['BYMONTHDAY'])) {
// @TODO: ActiveSync doesn't support multi-valued month days,
// should we replicate the recurrence element for each day of month?
$month_day = array_shift(explode(',', $r['BYMONTHDAY']));
$recurrence['type'] = self::RECUR_TYPE_YEARLY;
$recurrence['dayOfMonth'] = $month_day;
$recurrence['monthOfYear'] = $month;
}
else {
$recurrence['type'] = self::RECUR_TYPE_YEARLY;
$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'] ?: 1;
if (!empty($r['UNTIL'])) {
$recurrence['until'] = self::date_from_kolab($r['UNTIL']);
}
else if (!empty($r['COUNT'])) {
$recurrence['occurrences'] = $r['COUNT'];
}
$class = 'Syncroton_Model_' . $type . 'Recurrence';
$result['recurrence'] = new $class($recurrence);
// Tasks do not support exceptions
if ($type == 'Event') {
$result['exceptions'] = $this->exceptions_from_kolab($collection, $data);
}
}
/**
* Convert ActiveSync event/task recurrence into Kolab
*/
protected function recurrence_to_kolab($data, $folderid, $timezone = null)
{
if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence) || !isset($data->recurrence->type)) {
return null;
}
$recurrence = $data->recurrence;
$type = $recurrence->type;
switch ($type) {
case self::RECUR_TYPE_DAILY:
break;
case self::RECUR_TYPE_WEEKLY:
$rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek);
break;
case self::RECUR_TYPE_MONTHLY:
$rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
break;
case self::RECUR_TYPE_MONTHLY_DAYN:
$week = $recurrence->weekOfMonth;
$day = $recurrence->dayOfWeek;
$byDay = $week == 5 ? -1 : $week;
$byDay .= $this->bitmask2day($day);
$rrule['BYDAY'] = $byDay;
break;
case self::RECUR_TYPE_YEARLY:
$rrule['BYMONTH'] = $recurrence->monthOfYear;
$rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
break;
case self::RECUR_TYPE_YEARLY_DAYN:
$rrule['BYMONTH'] = $recurrence->monthOfYear;
$week = $recurrence->weekOfMonth;
$day = $recurrence->dayOfWeek;
$byDay = $week == 5 ? -1 : $week;
$byDay .= $this->bitmask2day($day);
$rrule['BYDAY'] = $byDay;
break;
}
$rrule['FREQ'] = $this->recurTypeMap[$type];
$rrule['INTERVAL'] = isset($recurrence->interval) ? $recurrence->interval : 1;
if (isset($recurrence->until)) {
if ($timezone) {
$recurrence->until->setTimezone($timezone);
}
$rrule['UNTIL'] = $recurrence->until;
}
else if (!empty($recurrence->occurrences)) {
$rrule['COUNT'] = $recurrence->occurrences;
}
// recurrence exceptions (not supported by Tasks)
if ($data instanceof Syncroton_Model_Event) {
$this->exceptions_to_kolab($data, $rrule, $folderid, $timezone);
}
return $rrule;
}
/**
* Convert Kolab event recurrence exceptions into ActiveSync
*/
protected function exceptions_from_kolab($collection, $data)
{
if (empty($data['recurrence']['EXCEPTIONS']) && empty($data['recurrence']['EXDATE'])) {
return null;
}
$ex_list = array();
// exceptions (modified occurences)
foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) {
$exception['_mailbox'] = $data['_mailbox'];
$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']);
// @TODO: 'thisandfuture=true' is not supported in Activesync
// we'd need to slit the event into two separate events
$ex_list[] = new Syncroton_Model_EventException($ex);
}
// exdate (deleted occurences)
foreach ((array)$data['recurrence']['EXDATE'] as $exception) {
if (!($exception instanceof DateTime)) {
continue;
}
$ex = array(
'deleted' => 1,
'exceptionStartTime' => self::set_exception_time($exception, $data['_start']),
);
$ex_list[] = new Syncroton_Model_EventException($ex);
}
return $ex_list;
}
/**
* Convert ActiveSync event recurrence exceptions into Kolab
*/
protected function exceptions_to_kolab($data, &$rrule, $folderid, $timezone = null)
{
$rrule['EXDATE'] = array();
$rrule['EXCEPTIONS'] = array();
// handle exceptions from recurrence
if (!empty($data->exceptions)) {
foreach ($data->exceptions as $exception) {
if ($exception->deleted) {
$date = clone $exception->exceptionStartTime;
if ($timezone) {
$date->setTimezone($timezone);
}
$date->setTime(0, 0, 0);
$rrule['EXDATE'][] = $date;
}
else {
$ex = $this->toKolab($exception, $folderid, null, $timezone);
if ($data->allDayEvent) {
$ex['allday'] = 1;
}
$rrule['EXCEPTIONS'][] = $ex;
}
}
}
if (empty($rrule['EXDATE'])) {
unset($rrule['EXDATE']);
}
if (empty($rrule['EXCEPTIONS'])) {
unset($rrule['EXCEPTIONS']);
}
}
/**
* 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)
{
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($uid);
$tags = array_filter(array_map(function($v) { return $v['name']; }, $tags));
// merge result with old categories
if (!empty($categories)) {
$tags = array_unique(array_merge($tags, (array) $categories));
}
return $tags;
}
/**
* Set tags to kolab object
*/
protected function setKolabTags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Converts string of days (TU,TH) to bitmask used by ActiveSync
*
* @param string $days
*
* @return int
*/
protected function day2bitmask($days)
{
$days = explode(',', $days);
$result = 0;
foreach ($days as $day) {
$result = $result + $this->recurDayMap[$day];
}
return $result;
}
/**
* Convert bitmask used by ActiveSync to string of days (TU,TH)
*
* @param int $days
*
* @return string
*/
protected function bitmask2day($days)
{
$days_arr = array();
for ($bitmask = 1; $bitmask <= self::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) {
$dayMatch = $days & $bitmask;
if ($dayMatch === $bitmask) {
$days_arr[] = array_search($bitmask, $this->recurDayMap);
}
}
$result = implode(',', $days_arr);
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;
}
/**
* Returns all email addresses of the current user
*/
protected function user_emails()
{
$user_emails = kolab_sync::get_instance()->user->list_emails();
$user_emails = array_map(function($v) { return $v['email']; }, $user_emails);
return $user_emails;
}
/**
* 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;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Aug 25, 6:49 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
257746
Default Alt Text
(63 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment