Changeset View
Changeset View
Standalone View
Standalone View
lib/kolab_sync_data.php
Show First 20 Lines • Show All 478 Lines • ▼ Show 20 Lines | abstract class kolab_sync_data implements Syncroton_Data_IData | ||||
{ | { | ||||
$entry = $this->toKolab($entry, $folderId); | $entry = $this->toKolab($entry, $folderId); | ||||
$entry = $this->createObject($folderId, $entry); | $entry = $this->createObject($folderId, $entry); | ||||
if (empty($entry)) { | if (empty($entry)) { | ||||
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); | throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); | ||||
} | } | ||||
return $entry['uid']; | return $entry['_serverId']; | ||||
} | } | ||||
/** | /** | ||||
* update existing entry | * update existing entry | ||||
* | * | ||||
* @param string $folderId | * @param string $folderId | ||||
* @param string $serverId | * @param string $serverId | ||||
* @param SimpleXMLElement $entry | * @param SimpleXMLElement $entry | ||||
* | * | ||||
* @return string ID of the updated entry | * @return string ID of the updated entry | ||||
*/ | */ | ||||
public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) | public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry) | ||||
{ | { | ||||
$oldEntry = $this->getObject($folderId, $serverId); | $oldEntry = $this->getObject($folderId, $serverId); | ||||
if (empty($oldEntry)) { | if (empty($oldEntry)) { | ||||
throw new Syncroton_Exception_NotFound('id not found'); | throw new Syncroton_Exception_NotFound('id not found'); | ||||
} | } | ||||
$entry = $this->toKolab($entry, $folderId, $oldEntry); | $entry = $this->toKolab($entry, $folderId, $oldEntry); | ||||
$entry = $this->updateObject($folderId, $serverId, $entry); | $entry = $this->updateObject($folderId, $serverId, $entry); | ||||
if (empty($entry)) { | if (empty($entry)) { | ||||
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); | throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR); | ||||
} | } | ||||
return $entry['uid']; | return $entry['_serverId']; | ||||
} | } | ||||
/** | /** | ||||
* delete entry | * delete entry | ||||
* | * | ||||
* @param string $folderId | * @param string $folderId | ||||
* @param string $serverId | * @param string $serverId | ||||
* @param array $collectionData | * @param array $collectionData | ||||
▲ Show 20 Lines • Show All 74 Lines • ▼ Show 20 Lines | protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID) | ||||
break; | break; | ||||
case self::RESULT_UID: | case self::RESULT_UID: | ||||
$uids = $folder->get_uids($filter); | $uids = $folder->get_uids($filter); | ||||
if (!is_array($uids)) { | if (!is_array($uids)) { | ||||
$error = true; | $error = true; | ||||
} | } | ||||
else { | else if (!empty($uids)) { | ||||
$result = array_merge($result, $uids); | $result = array_merge($result, $this->applyServerId($uids, $folder)); | ||||
} | } | ||||
break; | break; | ||||
} | } | ||||
if ($error) { | if ($error) { | ||||
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); | throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); | ||||
} | } | ||||
Show All 21 Lines | protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID) | ||||
if ($count !== null && $count !== false) { | if ($count !== null && $count !== false) { | ||||
$result += (int) $count; | $result += (int) $count; | ||||
} | } | ||||
break; | break; | ||||
case self::RESULT_UID: | case self::RESULT_UID: | ||||
$uids = $folder->get_uids($tag_filter); | $uids = $folder->get_uids($tag_filter); | ||||
if (is_array($uids)) { | if (is_array($uids) && !empty($uids)) { | ||||
$result = array_unique(array_merge($result, $uids)); | $result = array_unique(array_merge($result, $this->applyServerId($uids, $folder))); | ||||
} | } | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
if (!$found) { | if (!$found) { | ||||
▲ Show 20 Lines • Show All 285 Lines • ▼ Show 20 Lines | abstract class kolab_sync_data implements Syncroton_Data_IData | ||||
protected function getObject($folderid, $entryid, &$folder = null) | protected function getObject($folderid, $entryid, &$folder = null) | ||||
{ | { | ||||
$folders = $this->extractFolders($folderid); | $folders = $this->extractFolders($folderid); | ||||
foreach ($folders as $folderid) { | foreach ($folders as $folderid) { | ||||
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); | $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); | ||||
$folder = $this->getFolderObject($foldername); | $folder = $this->getFolderObject($foldername); | ||||
if ($folder && $folder->valid && ($object = $folder->get_object($entryid))) { | 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; | $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; | return $object; | ||||
} | } | ||||
vanmeeuwen: Would there be a reason to not let `$folder->get_object($uid)` fail before doing the more… | |||||
Not Done Inline ActionsAs it is prefix match I don't think it would give as much more performance. Besides, after this change all UIDs are supposed to be CRC-prefixed. So, it is more a sanity check + string exploding. machniak: As it is prefix match I don't think it would give as much more performance. Besides, after this… | |||||
Not Done Inline ActionsOK, agreed. vanmeeuwen: OK, agreed. | |||||
} | } | ||||
} | } | ||||
} | |||||
/** | /** | ||||
* Saves the entry on the backend | * Saves the entry on the backend | ||||
*/ | */ | ||||
protected function createObject($folderid, $data) | protected function createObject($folderid, $data) | ||||
{ | { | ||||
if ($folderid == $this->defaultRootFolder) { | if ($folderid == $this->defaultRootFolder) { | ||||
$default = $this->getDefaultFolder(); | $default = $this->getDefaultFolder(); | ||||
Show All 14 Lines | protected function createObject($folderid, $data) | ||||
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); | $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); | ||||
$folder = $this->getFolderObject($foldername); | $folder = $this->getFolderObject($foldername); | ||||
if ($folder && $folder->valid && $folder->save($data)) { | if ($folder && $folder->valid && $folder->save($data)) { | ||||
if (!empty($tags)) { | if (!empty($tags)) { | ||||
$this->setKolabTags($data['uid'], $tags); | $this->setKolabTags($data['uid'], $tags); | ||||
} | } | ||||
$data['_serverId'] = $this->serverId($data['uid'], $folder); | |||||
return $data; | return $data; | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Updates the entry on the backend | * Updates the entry on the backend | ||||
*/ | */ | ||||
protected function updateObject($folderid, $entryid, $data) | protected function updateObject($folderid, $entryid, $data) | ||||
Show All 9 Lines | protected function updateObject($folderid, $entryid, $data) | ||||
unset($data['categories']); | unset($data['categories']); | ||||
} | } | ||||
if ($folder && $folder->valid && $folder->save($data)) { | if ($folder && $folder->valid && $folder->save($data)) { | ||||
if (isset($tags)) { | if (isset($tags)) { | ||||
$this->setKolabTags($data['uid'], $tags); | $this->setKolabTags($data['uid'], $tags); | ||||
} | } | ||||
$data['_serverId'] = $this->serverId($object['uid'], $folder); | |||||
return $data; | return $data; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Removes the entry from the backend | * Removes the entry from the backend | ||||
*/ | */ | ||||
protected function deleteObject($folderid, $entryid) | protected function deleteObject($folderid, $entryid) | ||||
{ | { | ||||
$object = $this->getObject($folderid, $entryid); | $object = $this->getObject($folderid, $entryid); | ||||
if ($object) { | if ($object) { | ||||
$folder = $this->getFolderObject($object['_mailbox']); | $folder = $this->getFolderObject($object['_mailbox']); | ||||
if ($folder && $folder->valid && $folder->delete($entryid)) { | if ($folder && $folder->valid && $folder->delete($object['uid'])) { | ||||
if ($this->tag_categories) { | if ($this->tag_categories) { | ||||
$this->setKolabTags($object['uid'], null); | $this->setKolabTags($object['uid'], null); | ||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
return false; | return false; | ||||
▲ Show 20 Lines • Show All 810 Lines • ▼ Show 20 Lines | protected function deviceTypeFilter($options) | ||||
} | } | ||||
else if (stripos($this->device->devicetype, $option) !== false) { | else if (stripos($this->device->devicetype, $option) !== false) { | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
return false; | 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); | |||||
Not Done Inline Actionsthis substr($uid, 0, 53) will actually constitute up to 54 characters, right? We only have 53 left. vanmeeuwen: this `substr($uid, 0, 53)` will actually constitute up to 54 characters, right? We only have 53… | |||||
Not Done Inline Actions3rd argument of substr() is requested length. machniak: 3rd argument of substr() is requested length. | |||||
Not Done Inline ActionsAh, I thought it was the end position, starting from 0 ;-) vanmeeuwen: Ah, I thought it was the end position, starting from 0 ;-) | |||||
} | |||||
/** | |||||
* 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; | |||||
} | |||||
} | } |
Would there be a reason to not let $folder->get_object($uid) fail before doing the more expensive preg_match()?
Could we clause with a strstr($uid, 'CRC') == 0 to clause the expensive preg_match() only happening against the sub-set of UIDs?