Projects
Kolab:16:Enterprise
kolab-syncroton
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 71
View file
kolab-syncroton.spec
Changed
@@ -43,7 +43,7 @@ %global upstream_version 2.4.2 Name: kolab-syncroton -Version: 2.4.2.31 +Version: 2.4.2.34 Release: 1%{?dist} Summary: ActiveSync for Kolab Groupware
View file
debian.changelog
Changed
@@ -1,4 +1,4 @@ -kolab-syncroton (2.4.2.31-0~kolab1) unstable; urgency=low +kolab-syncroton (2.4.2.34-0~kolab1) unstable; urgency=low * Release version 2.4.2
View file
kolab-syncroton-2.4.2.tar.gz/bin/analyzelogs.php
Added
@@ -0,0 +1,149 @@ +#!/usr/bin/env php +<?php +/* + +--------------------------------------------------------------------------+ + | Kolab Sync (ActiveSync for Kolab) | + | | + | Copyright (C) 2024, Apheleia IT AG <contact@apheleia-it.ch> | + | | + | 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: Christian Mollekopf <mollekopf@apheleia-it.ch> | + +--------------------------------------------------------------------------+ +*/ + + +define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__) . '/../') . '/'); + +// Define include path +$include_path = RCUBE_INSTALL_PATH . 'lib' . PATH_SEPARATOR; +$include_path .= RCUBE_INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR; +$include_path .= ini_get('include_path'); +set_include_path($include_path); + +require_once "Syncroton/Command/ICommand.php"; +require_once "Syncroton/Command/Wbxml.php"; +require_once "Syncroton/Command/Sync.php"; +require_once "Syncroton/Command/Ping.php"; +require_once "Syncroton/Command/MoveItems.php"; +require_once "Syncroton/Command/FolderSync.php"; + +$filename = $argv1; + +$content = file_get_contents($filename); + +// Split up the log files into chunks that hopefully match the commands +$parts = preg_split("/\.*\: " . preg_quote("DEBUG Syncroton_Server::handle::65 REQUEST METHOD: POST", '/') . "/", $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + +function getStatusConstants($classname) +{ + $reflect = new ReflectionClass($classname); + $result = $reflect->getConstants(); + $result = array_filter($result, function ($val) { + return str_starts_with($val, "STATUS_"); + }, ARRAY_FILTER_USE_KEY); + $result = array_flip($result); + return $result; +} + +function explainStatus($command, $status) +{ + if (!$status) { + return "none"; + } + switch ($command) { + case "Ping": + $result = getStatusConstants("Syncroton_Command_Ping"); + return $result$status ?? "Unknown"; + case "Sync": + $result = getStatusConstants("Syncroton_Command_Sync"); + return $result$status ?? "Unknown"; + case "MoveItems": + $result = getStatusConstants("Syncroton_Command_MoveItems"); + return $result$status ?? "Unknown"; + case "FolderSync": + $result = getStatusConstants("Syncroton_Command_FolderSync"); + return $result$status ?? "Unknown"; + } + return "Unknown command"; +} + +foreach ($parts as $part) { + preg_match('/\(.*)\: /', $part, $matches); + $timestamp = $matches1; + + preg_match('/\command\ => (.*)/', $part, $matches); + $command = $matches1; + + preg_match('/\<Status\>(.*)\<\/Status\>/', $part, $matches); + $status = $matches1 ?? null; + + $statusExplained = explainStatus($command, $status); + + print(" Command: " . str_pad($command, 10) . str_pad("\tStatus: $status ($statusExplained)", 45) . "\tTimestamp: $timestamp\n"); + if ($command == "Sync") { + // Find collections within this sync + // 25-Sep-2024 09:16:35.347730: INFO Syncroton_Command_Sync::handle::221 SyncKey is 7301 Class: Email CollectionId: 38b950ebd62cd9a66929c89615d0fc04 + if (preg_match_all('/SyncKey is (.*) Class: (.*) CollectionId: (.*)/', $part, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + foreach ($matches as $set) { + $offset = $set01; + $collectionId = $set30; + $class = $set20; + $synckey = $set10; + print(str_pad(" Collection: $collectionId ($class)", 58) . "\tSyncKey: $synckey\n"); + + //Find the offset for this collections messages + if (preg_match("/Processing $collectionId\.\.\./", $part, $match, PREG_OFFSET_CAPTURE, $offset)) { + // print_r($match); + $offset = $match01 ?? null; + //Find the actual changes + if ($offset && preg_match('/found \(added\/changed\/deleted\) (.*)\/(.*)\/(.*) entries for sync from server to client/', $part, $changesMatch, PREG_OFFSET_CAPTURE, $offset)) { + // If the offset is too large we are looking at the next collection. + if ($changesMatch01 - $offset < 200) { + print(" " . $changesMatch00 . "\n"); + } + } + } + //TODO We could figure out what the diff per collection was in terms of synckey to the last sync + //TODO We could figure out what we actually return in the response compared to the detected changeset + //TODO Warn if a collection is repeatedly synced with the same synckey, but changes are detected. It may be stuck in a sync loop. + } + } + + // Detect entries that are being added from the client + if (preg_match_all('/found (.*) entries to be added on server/', $part, $matches)) { + foreach ($matches0 ?? as $match) { + print(" " . $match . "\n"); + } + } + + if (preg_match_all('/found (.*) entries to be updated on server/', $part, $matches)) { + foreach ($matches0 ?? as $match) { + print(" " . $match . "\n"); + } + } + + if (preg_match_all('/found (.*) entries to be deleted on server/', $part, $matches)) { + foreach ($matches0 ?? as $match) { + print(" " . $match . "\n"); + } + } + } + //TODO on Sync: + //* number of Add/Change/Remove from client and from server + //* Synckey + //* list involved folders + //TODO on Sync: + //* Reason for interruption +}
View file
kolab-syncroton-2.4.2.tar.gz/bin/inspect.php
Changed
@@ -41,6 +41,38 @@ // include global functions from Roundcube Framework require_once 'Roundcube/bootstrap.php'; + +function filterTypeToIMAPSearch($filter_type = 0) +{ + switch ($filter_type) { + case 1: + $mod = '-1 day'; + break; + case 2: + $mod = '-3 days'; + break; + case 3: + $mod = '-1 week'; + break; + case 4: + $mod = '-2 weeks'; + break; + case 5: + $mod = '-1 month'; + break; + } + + if (!empty($mod)) { + $dt = new DateTime('now', new DateTimeZone('UTC')); + $dt->modify($mod); + // RFC3501: IMAP SEARCH + return 'SINCE ' . $dt->format('d-M-Y'); + } + + return ""; +} + + $opts = rcube_utils::get_opt( 'e' => 'email', 'p' => 'adminpassword', @@ -78,8 +110,8 @@ 'ssl' => 'verify_peer_name' => false, 'verify_peer' => false, - 'allow_self_signed' => true - + 'allow_self_signed' => true, + , ; ini_set('display_errors', 1); @@ -163,17 +195,23 @@ $result$device_id'folders'$folder'id' = "counter" => $data'counter', "lastsync" => $data'lastsync', - "lastfiltertype" => $data'lastfiltertype' ?? null, "modseq" => $data'extra_data' ? json_decode($data'extra_data')->modseq : null, ; } $result$device_id'folders'$folder'id''name' = $folder'displayname'; + $result$device_id'folders'$folder'id''class' = $folder'class'; + $result$device_id'folders'$folder'id''lastfiltertype' = $folder'lastfiltertype' ?? null; $imap->select($folder'displayname'); $result$device_id'folders'$folder'id''imapModseq' = $imap->data'HIGHESTMODSEQ' ?? null; - $index = $imap->search($folder'displayname', 'ALL UNDELETED', false, 'COUNT'); + $index = $imap->search( + $folder'displayname', + 'ALL UNDELETED ' . filterTypeToIMAPSearch($folder'lastfiltertype'), + false, + 'COUNT' + ); if (!$index->is_error()) { $result$device_id'folders'$folder'id''imapMessagecount' = $index->count(); } @@ -195,26 +233,54 @@ var_export($result); } -function println($output) { +function println($output) +{ print("{$output}\n"); } -function filterType($value) { - if (!$value) { - return "No filter"; - } - switch($value) { - case 0: return "No filter"; - case 1: return "1 day"; - case 2: return "3 days"; - case 3: return "1 week"; - case 4: return "2 weeks"; - case 5: return "1 month"; - case 6: return "3 months"; - case 7: return "6 months"; - case 8: return "Filter by incomplete tasks"; - } - return "Unknown value: $value"; +function filterType($value) +{ + if (!$value) { + return "No filter"; + } + switch($value) { + case 0: return "No filter"; + case 1: return "1 day"; + case 2: return "3 days"; + case 3: return "1 week"; + case 4: return "2 weeks"; + case 5: return "1 month"; + case 6: return "3 months (WARNING: not implemented)"; + case 7: return "6 months (WARNING: not implemented)"; + case 8: return "Filter by incomplete tasks"; + } + return "Unknown value: $value"; +} + +function getContentUids($db, $device_id, $folder_id) +{ + $contentSelect = $db->query( + "SELECT contentid FROM `syncroton_content`" + . " WHERE `device_id` = ? AND `folder_id` = ? AND `is_deleted` = 0", + $device_id, + $folder_id + ); + + $contentUids = ; + while ($content = $db->fetch_assoc($contentSelect)) { + $contentUids = explode('::', $content'contentid')1; + } + return $contentUids; +} + +function getImapUids($imap, $folder, $lastfiltertype) +{ + $imap->select($folder); + $index = $imap->search($folder, 'ALL UNDELETED ' . filterTypeToIMAPSearch($lastfiltertype), true); + if (!$index->is_error()) { + return $index->get(); + } + return ; } println(""); @@ -239,6 +305,39 @@ println(" Last sync: " . ($folder'lastsync' ?? "None")); println(" Number of syncs: " . ($folder'counter' ?? "None")); println(" Filter type: " . filterType($folder'lastfiltertype' ?? null)); + + if (($folder'class' == "Email") && ($folder'counter' ?? false) && $messageCount != $totalCount && ($modseq == "none" || $modseq == $imapModseq)) { + if (($folder'lastfiltertype' ?? false) && $messageCount > $totalCount) { + // This doesn't have to indicate an issue, since the timewindow of the filter wanders, so some messages that have been synchronized may no longer match the window. + } else { + println(" Issue Detected: The sync state seems to be inconsistent. The device should be fully synced, but the sync counts differ."); + println(" There are $messageCount ContentParts (should match number of messages on the device), but $totalCount messages in IMAP matching the filter."); + + $contentUids = getContentUids($db, $deviceId, $folderId); + $imapUids = getImapUids($imap, $folder'name', $folder'lastfiltertype' ?? null); + + $entries = array_diff($imapUids, $contentUids); + if (!empty($entries)) { + println(" The following messages are on the server, but not the device:"); + foreach ($entries as $uid) { + println(" $uid"); + //TODO get details from imap? + } + } + + $entries = array_diff($contentUids, $imapUids); + if (!empty($entries)) { + println(" The following messages are on the device, but not the server:"); + foreach ($entries as $uid) { + println(" $uid"); + //TODO get details from the content part? + //TODO display creation_synckey? + } + } + println(""); + } + } + println(""); } }
View file
kolab-syncroton-2.4.2.tar.gz/bin/resync.php
Added
@@ -0,0 +1,116 @@ +#!/usr/bin/php +<?php +/* + +--------------------------------------------------------------------------+ + | Kolab Sync (ActiveSync for Kolab) | + | | + | Copyright (C) 2024, Apheleia IT AG <contact@apheleia-it.ch> | + | | + | 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@apheleia-it.ch> | + +--------------------------------------------------------------------------+ +*/ + +define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__) . '/../') . '/'); +define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/plugins/'); + +// Define include path +$include_path = RCUBE_INSTALL_PATH . 'lib' . PATH_SEPARATOR; +$include_path .= RCUBE_INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR; +$include_path .= ini_get('include_path'); +set_include_path($include_path); + +// include composer autoloader (if available) +if (@file_exists(RCUBE_INSTALL_PATH . 'vendor/autoload.php')) { + require RCUBE_INSTALL_PATH . 'vendor/autoload.php'; +} + +// include global functions from Roundcube Framework +require_once 'Roundcube/bootstrap.php'; + +$opts = rcube_utils::get_opt( + 'o' => 'owner', + 'f' => 'folder', + 'd' => 'deviceid', + 't' => 'devicetype', // e.g. WindowsOutlook15 or iPhone +); + +$rcube = \rcube::get_instance(); +$db = $rcube->get_dbh(); + +if (empty($opts'owner')) { + rcube::raise_error("Owner not specified (--owner).", false, true); +} +if (empty($opts'folder')) { + rcube::raise_error("Folder name not specified (--folder).", false, true); +} + +$select = $db->query( + "SELECT `user_id` FROM `users` WHERE `username` = ? ORDER BY `user_id` DESC", + \strtolower($opts'owner') +); + +if ($data = $db->fetch_assoc($select)) { + $userid = $data'user_id'; +} else { + rcube::raise_error("User not found in Roundcube database.", false, true); +} + +$devices = ; +if (!empty($opts'deviceid')) { + $select = $db->query( + "SELECT `id` FROM `syncroton_device` WHERE `owner_id` = ? AND `deviceid` = ?", + $userid, + $opts'deviceid' + ); + while ($record = $db->fetch_assoc($select)) { + $devices = $record'id'; + } +} elseif (!empty($opts'devicetype')) { + $select = $db->query( + "SELECT `id` FROM `syncroton_device` WHERE `owner_id` = ? AND `devicetype` = ?", + $userid, + $opts'devicetype' + ); + while ($record = $db->fetch_assoc($select)) { + $devices = $record'id'; + } +} else { + $select = $db->query("SELECT `id` FROM `syncroton_device` WHERE `owner_id` = ?", $userid); + while ($record = $db->fetch_assoc($select)) { + $devices = $record'id'; + } +} + +if (empty($devices)) { + rcube::raise_error("Device not found.", false, true); +} + +// TODO: Support not only top-level folders + +$select = $db->query( + "SELECT `id`, `displayname`, `folderid` FROM `syncroton_folder`" + . " WHERE `device_id` IN (" . $db->array2list($devices) . ")" + . " AND `parentid` = '0' AND `displayname` = " . $db->quote($opts'folder') +); + +while ($record = $db->fetch_assoc($select)) { + if (!empty($opts'dry-run')) { + print("DRY-RUN {$record'displayname'} ({$record'id'}:{$record'folderid'})\n"); + } else { + $db->query("UPDATE `syncroton_folder` SET `resync` = 1 WHERE id = ?", $record'id'); + print("{$record'displayname'} ({$record'id'}:{$record'folderid'})\n"); + } +}
View file
kolab-syncroton-2.4.2.tar.gz/docs/SQL/mysql.initial.sql
Changed
@@ -44,6 +44,7 @@ `displayname` varchar(254) NOT NULL, `type` int(11) NOT NULL, `creation_time` datetime NOT NULL, + `creation_synckey` int(11) NOT NULL DEFAULT '0', `lastfiltertype` int(11) DEFAULT NULL, `supportedfields` longblob DEFAULT NULL, PRIMARY KEY (`id`), @@ -115,4 +116,4 @@ PRIMARY KEY(`name`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; -INSERT INTO `system` (`name`, `value`) VALUES ('syncroton-version', '2024031100'); +INSERT INTO `system` (`name`, `value`) VALUES ('syncroton-version', '2024102300');
View file
kolab-syncroton-2.4.2.tar.gz/docs/SQL/mysql/2024101700.sql
Added
@@ -0,0 +1,1 @@ +ALTER TABLE `syncroton_folder` ADD `resync` tinyint(1) DEFAULT NULL;
View file
kolab-syncroton-2.4.2.tar.gz/docs/SQL/mysql/2024102300.sql
Added
@@ -0,0 +1,1 @@ +ALTER TABLE `syncroton_folder` ADD `creation_synckey` int(11) NOT NULL DEFAULT '0';
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Backend/IFolder.php
Changed
@@ -32,9 +32,10 @@ * * @param Syncroton_Model_Device|string $deviceId * @param string $class + * @param int $syncKey * @return array */ - public function getFolderState($deviceId, $class); + public function getFolderState($deviceId, $class, $syncKey); /** * delete all stored folderId's for given device
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Command/FolderCreate.php
Changed
@@ -100,6 +100,7 @@ $this->_folder->class = $folder->class; $this->_folder->deviceId = $this->_device->id; $this->_folder->creationTime = $this->_syncTimeStamp; + $this->_folder->creationSynckey = $this->_syncState->counter; // Check if the folder already exists to avoid a duplicate insert attempt in db try {
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Command/FolderSync.php
Changed
@@ -94,6 +94,9 @@ } if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) { + if ($this->_logger instanceof Zend_Log) { + $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalidating sync state"); + } $this->_syncStateBackend->resetState($this->_device, 'FolderSync'); } } @@ -143,7 +146,7 @@ $serverFolders = $dataController->getAllFolders(); // retrieve all folders sent to client - $clientFolders = $this->_folderBackend->getFolderState($this->_device, $class); + $clientFolders = $this->_folderBackend->getFolderState($this->_device, $class, $this->_syncState->counter); if ($this->_syncState->counter > 0) { // retrieve all folders changed since last sync @@ -194,6 +197,7 @@ } else { $add = $serverFolders$serverFolderId; $add->creationTime = $this->_syncTimeStamp; + $add->creationSynckey = $this->_syncState->counter + 1; $add->deviceId = $this->_device->id; unset($add->id); } @@ -234,6 +238,12 @@ } } + // Handle folders set for forced re-sync, we'll send a delete action to the client, + // but because the folder is still existing and subscribed on the backend it should + // "immediately" be added again (and re-synced). + $forceDeleteIds = array_keys(array_filter($clientFolders, function ($f) { return !empty($f->resync); })); + $serverFoldersIds = array_diff($serverFoldersIds, $forceDeleteIds); + // calculate deleted entries $serverDiff = array_diff($clientFoldersIds, $serverFoldersIds); foreach ($serverDiff as $serverFolderId) { @@ -262,7 +272,12 @@ // store folder in backend if (empty($folder->id)) { - $this->_folderBackend->create($folder); + try { + $this->_folderBackend->create($folder); + } catch(Exception $zdse) { + //This can happen if we rerun a previous sync-key + $this->_folderBackend->update($folder); + } } } @@ -281,10 +296,10 @@ $this->_folderBackend->delete($folder); } - if (empty($this->_syncState->id)) { - $this->_syncStateBackend->create($this->_syncState); - } else { - $this->_syncStateBackend->update($this->_syncState); + // Only create this syncstate if it isn't already existing (which happens if we a sync key is re-sent) + if (!$this->_syncStateBackend->haveNext($this->_device, 'FolderSync', $this->_syncState->counter - 1)) { + // Keep previous sync states in case a sync key is re-sent + $this->_syncStateBackend->create($this->_syncState, true); } return $this->_outputDom;
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Command/MoveItems.php
Changed
@@ -59,11 +59,11 @@ $response = $moves->appendChild($this->_outputDom->createElementNS('uri:Move', 'Response')); $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'SrcMsgId', $move'srcMsgId')); - try { - if ($move'srcFldId' === $move'dstFldId') { - throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::SAME_FOLDER); - } + if ($this->_logger instanceof Zend_Log) { + $this->_logger->info(__METHOD__ . '::' . __LINE__ . " Moving from " . $move'srcFldId' . " to " . $move'dstFldId'); + } + try { try { $sourceFolder = $this->_folderBackend->getFolder($this->_device, $move'srcFldId'); } catch (Syncroton_Exception_NotFound $e) { @@ -76,19 +76,30 @@ throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION); } + if ($move'srcFldId' === $move'dstFldId') { + throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::SAME_FOLDER); + } + $dataController = Syncroton_Data_Factory::factory($sourceFolder->class, $this->_device, $this->_syncTimeStamp); $newId = $dataController->moveItem($move'srcFldId', $move'srcMsgId', $move'dstFldId'); - if (!$newId) { + // We don't actually know what the reason was that this failed, but from the resolution description this error seems to make the most sense, + // and we rule out most other reasons before. throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); } $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', Syncroton_Command_MoveItems::STATUS_SUCCESS)); $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'DstMsgId', $newId)); - } catch (Syncroton_Exception_Status $e) { + } catch (Syncroton_Exception_Status_MoveItems $e) { + if ($this->_logger instanceof Zend_Log) { + $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Move failed: " . $e->getMessage()); + } $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', $e->getCode())); } catch (Exception $e) { - $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', Syncroton_Exception_Status::SERVER_ERROR)); + if ($this->_logger instanceof Zend_Log) { + $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Move failed: " . $e->getMessage()); + } + $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', Syncroton_Exception_Status_MoveItems::FOLDER_LOCKED)); } }
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Command/Ping.php
Changed
@@ -124,7 +124,7 @@ do { // take a break to save battery lifetime call_user_func($sleepCallback); - sleep(Syncroton_Registry::getPingTimeout()); + sleep(min(Syncroton_Registry::getPingTimeout(), $lifeTime)); // make sure the connection is still alive, abort otherwise if (connection_aborted()) {
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Command/Sync.php
Changed
@@ -467,6 +467,93 @@ } } + private function getServerModifications($dataController, $collectionData, $clientModifications) + { + $serverModifications = + 'added' => , + 'changed' => , + 'deleted' => , + ; + + // We first use hasChanges because it has a fast path for when there are no changes by fetching the count of messages only. + // However, in all other cases we will end up fetching the same entries as below, which is less than ideal. + // TODO: We should create a new method, which checks if there are no changes, and otherwise just let the code below figure out + // if there are any changes to process. + if (!$dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState)) { + return $serverModifications; + } + + // update _syncTimeStamp as $dataController->hasChanges might have spent some time + $this->_syncTimeStamp = new DateTime('now', new DateTimeZone('UTC')); + + // fetch entries added since last sync + $allClientEntries = $this->_contentStateBackend->getFolderState( + $this->_device, + $collectionData->folder, + $collectionData->syncState->counter + ); + + // fetch entries changed since last sync + $allChangedEntries = $dataController->getChangedEntries( + $collectionData->collectionId, + $collectionData->syncState, + $collectionData->options'filterType' + ); + + // fetch all entries + $allServerEntries = $dataController->getServerEntries( + $collectionData->collectionId, + $collectionData->options'filterType' + ); + + // add entries + $serverDiff = array_diff($allServerEntries, $allClientEntries); + // add entries which produced problems during delete from client + $serverModifications'added' = $clientModifications'forceAdd'; + // add entries not yet sent to client + $serverModifications'added' = array_unique(array_merge($serverModifications'added', $serverDiff)); + + // @todo still needed? + foreach($serverModifications'added' as $id => $serverId) { + // skip entries added by client during this sync session + if(isset($clientModifications'added'$serverId) && !isset($clientModifications'forceAdd'$serverId)) { + if ($this->_logger instanceof Zend_Log) { + $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId); + } + unset($serverModifications'added'$id); + } + } + + // entries to be deleted + $serverModifications'deleted' = array_diff($allClientEntries, $allServerEntries); + + // entries changed since last sync + $serverModifications'changed' = array_merge($allChangedEntries, $clientModifications'forceChange'); + + foreach($serverModifications'changed' as $id => $serverId) { + // skip entry, if it got changed by client during current sync + if(isset($clientModifications'changed'$serverId) && !isset($clientModifications'forceChange'$serverId)) { + if ($this->_logger instanceof Zend_Log) { + $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId); + } + unset($serverModifications'changed'$id); + } + // skip entry, make sure we don't sent entries already added by client in this request + elseif (isset($clientModifications'added'$serverId) && !isset($clientModifications'forceAdd'$serverId)) { + if ($this->_logger instanceof Zend_Log) { + $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped change for added entry: " . $serverId); + } + unset($serverModifications'changed'$id); + } + } + + // entries comeing in scope are already in $serverModifications'added' and do not need to + // be send with $serverCanges + $serverModifications'changed' = array_diff($serverModifications'changed', $serverModifications'added'); + + return $serverModifications; + } + /** * (non-PHPdoc) * @see Syncroton_Command_Wbxml::getResponse() @@ -663,18 +750,17 @@ ; $status = self::STATUS_SUCCESS; - $hasChanges = 0; if ($collectionData->getChanges === true) { // continue sync session? if(is_array($collectionData->syncState->pendingdata)) { $serverModifications = $collectionData->syncState->pendingdata; if ($this->_logger instanceof Zend_Log) { - $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state: (added/changed/deleted) " . count($serverModifications'added') . '/' . count($serverModifications'changed') . '/' . count($serverModifications'deleted') . ' entries for sync from server to client'); + $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state."); } } else { try { - $hasChanges = $dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState); + $serverModifications = $this->getServerModifications($dataController, $collectionData, $clientModifications); } catch (Syncroton_Exception_NotFound $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folder changes checking failed (not found): " . $e->getTraceAsString()); @@ -686,85 +772,6 @@ $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Folder changes checking failed: " . $e->getMessage()); } - // Prevent from removing client entries when getServerEntries() fails - // @todo: should we break the loop here? - $status = self::STATUS_SERVER_ERROR; - } - } - - if ($hasChanges) { - // update _syncTimeStamp as $dataController->hasChanges might have spent some time - $this->_syncTimeStamp = new DateTime('now', new DateTimeZone('UTC')); - - try { - // fetch entries added since last sync - $allClientEntries = $this->_contentStateBackend->getFolderState( - $this->_device, - $collectionData->folder, - $collectionData->syncState->counter - ); - - // fetch entries changed since last sync - $allChangedEntries = $dataController->getChangedEntries( - $collectionData->collectionId, - $collectionData->syncState, - $collectionData->options'filterType' - ); - - // fetch all entries - $allServerEntries = $dataController->getServerEntries( - $collectionData->collectionId, - $collectionData->options'filterType' - ); - - // add entries - $serverDiff = array_diff($allServerEntries, $allClientEntries); - // add entries which produced problems during delete from client - $serverModifications'added' = $clientModifications'forceAdd'; - // add entries not yet sent to client - $serverModifications'added' = array_unique(array_merge($serverModifications'added', $serverDiff)); - - // @todo still needed? - foreach($serverModifications'added' as $id => $serverId) { - // skip entries added by client during this sync session - if(isset($clientModifications'added'$serverId) && !isset($clientModifications'forceAdd'$serverId)) { - if ($this->_logger instanceof Zend_Log) { - $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId); - } - unset($serverModifications'added'$id); - } - } - - // entries to be deleted - $serverModifications'deleted' = array_diff($allClientEntries, $allServerEntries); - - // entries changed since last sync - $serverModifications'changed' = array_merge($allChangedEntries, $clientModifications'forceChange'); - - foreach($serverModifications'changed' as $id => $serverId) { - // skip entry, if it got changed by client during current sync - if(isset($clientModifications'changed'$serverId) && !isset($clientModifications'forceChange'$serverId)) { - if ($this->_logger instanceof Zend_Log) { - $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId); - } - unset($serverModifications'changed'$id); - } - // skip entry, make sure we don't sent entries already added by client in this request - elseif (isset($clientModifications'added'$serverId) && !isset($clientModifications'forceAdd'$serverId)) { - if ($this->_logger instanceof Zend_Log) { - $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped change for added entry: " . $serverId); - } - unset($serverModifications'changed'$id); - } - } - - // entries comeing in scope are already in $serverModifications'added' and do not need to - // be send with $serverCanges - $serverModifications'changed' = array_diff($serverModifications'changed', $serverModifications'added'); - } catch (Exception $e) { - if ($this->_logger instanceof Zend_Log) { - $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getMessage()); - } if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getTraceAsString()); } @@ -773,10 +780,10 @@ // @todo: should we break the loop here? $status = self::STATUS_SERVER_ERROR; } + } - if ($this->_logger instanceof Zend_Log) { - $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverModifications'added') . '/' . count($serverModifications'changed') . '/' . count($serverModifications'deleted') . ' entries for sync from server to client'); - } + if ($this->_logger instanceof Zend_Log) { + $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverModifications'added') . '/' . count($serverModifications'changed') . '/' . count($serverModifications'deleted') . ' entries for sync from server to client'); } }
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Model/Folder.php
Changed
@@ -33,7 +33,9 @@ 'ownerId' => 'type' => 'string', 'class' => 'type' => 'string', 'creationTime' => 'type' => 'datetime', + 'creationSynckey' => 'type' => 'number', 'lastfiltertype' => 'type' => 'number', + 'resync' => 'type' => 'number', , ; }
View file
kolab-syncroton-2.4.2.tar.gz/lib/ext/Syncroton/Model/IFolder.php
Changed
@@ -21,6 +21,7 @@ * @property string $parentId * @property string $displayName * @property DateTime $creationTime + * @property int $creationSynckey * @property int $lastfiltertype * @property int $type */
View file
kolab-syncroton-2.4.2.tar.gz/lib/kolab_sync.php
Changed
@@ -135,13 +135,15 @@ $userid = $this->authenticate($_SERVER'PHP_AUTH_USER', $_SERVER'PHP_AUTH_PW'); } - $this->plugins->exec_hook('ready', 'task' => 'syncroton'); + if (!empty($userid)) { + $this->plugins->exec_hook('ready', 'task' => 'syncroton'); - // Set log directory per-user (again, in case the username changed above) - $this->set_log_dir(); + // Set log directory per-user (again, in case the username changed above) + $this->set_log_dir(); - // Save user password for Roundcube Framework - $this->password = $_SERVER'PHP_AUTH_PW'; + // Save user password for Roundcube Framework + $this->password = $_SERVER'PHP_AUTH_PW'; + } // Register Syncroton backends/callbacks Syncroton_Registry::set(Syncroton_Registry::LOGGERBACKEND, $this->logger); @@ -171,7 +173,7 @@ Syncroton_Registry::set(Syncroton_Registry::MAX_COLLECTIONS, (int) $this->config->get('activesync_max_folders', 100)); // Run Syncroton - $syncroton = new Syncroton_Server($userid); + $syncroton = new Syncroton_Server($userid ?? null); $syncroton->handle(); }
View file
kolab-syncroton-2.4.2.tar.gz/lib/kolab_sync_backend_folder.php
Changed
@@ -50,15 +50,19 @@ * * @param Syncroton_Model_Device|string $deviceid Device object or identifier * @param string $class Class name + * @param int $syncKey Sync key * * @return array List of object identifiers */ - public function getFolderState($deviceid, $class) + public function getFolderState($deviceid, $class, $syncKey = null) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $where = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $where = $this->db->quote_identifier('class') . ' = ' . $this->db->quote($class); + if ($syncKey) { + $where = $this->db->quote_identifier('creation_synckey') . ' < ' . $this->db->quote($syncKey + 1); + } $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); $result = ; @@ -88,8 +92,11 @@ $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); $folder = $this->db->fetch_assoc($select); + if (!empty($folder'resync')) { + throw new Syncroton_Exception_NotFound("Folder $folderid not found because of resync"); + } if (empty($folder)) { - throw new Syncroton_Exception_NotFound('Folder not found'); + throw new Syncroton_Exception_NotFound("Folder $folderid not found"); } return $this->get_object($folder); @@ -120,6 +127,18 @@ // Reset imap cache so we work with up-to-date folders list rcube::get_instance()->get_storage()->clear_cache('mailboxes', true); + // Retrieve all folders already sent to the client + $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE `device_id` = ?", $device->id); + + while ($folder = $this->db->fetch_assoc($select)) { + if (!empty($folder'resync')) { + // Folder re-sync requested + return true; + } + + $client_folders$folder'folderid' = $this->get_object($folder); + } + foreach ($folder_classes as $class) { try { // retrieve all folders available in data backend @@ -132,13 +151,6 @@ } } - // retrieve all folders sent to the client - $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE `device_id` = ?", $device->id); - - while ($folder = $this->db->fetch_assoc($select)) { - $client_folders$folder'folderid' = $this->get_object($folder); - } - ksort($client_folders); ksort($server_folders);
View file
kolab-syncroton-2.4.2.tar.gz/lib/kolab_sync_data_email.php
Changed
@@ -674,7 +674,7 @@ $result = ; if (!is_array($list)) { - throw new Syncroton_Exception_NotFound('Folder not found'); + throw new Syncroton_Exception_NotFound("Folder $folder_id not found: no folders available"); } // device supports multiple folders? @@ -691,7 +691,7 @@ } if (empty($result)) { - throw new Syncroton_Exception_NotFound('Folder not found'); + throw new Syncroton_Exception_NotFound("Folder $folder_id not found."); } return $result; @@ -1137,7 +1137,7 @@ $message = $this->getObject($fileReference); if (!$message) { - throw new Syncroton_Exception_NotFound('Message not found'); + throw new Syncroton_Exception_NotFound("Message $fileReference not found"); } $part = $message->mime_parts$part_id;
View file
kolab-syncroton-2.4.2.tar.gz/lib/kolab_sync_logger.php
Changed
@@ -161,8 +161,8 @@ } foreach ($params as $key => $val) { - if ($val = $_GET$val) { - $device$key = $val; + if (isset($_GET$val)) { + $device$key = $_GET$val; } }
View file
kolab-syncroton-2.4.2.tar.gz/lib/kolab_sync_storage.php
Changed
@@ -1042,6 +1042,12 @@ // Use COPYUID feature (RFC2359) to get the new UID of the copied message if (empty($this->storage->conn->data'COPYUID')) { + // Check if the source item actually exists. Cyrus IMAP reports + // OK on a MOVE with an invalid UID, But COPYUID will be empty. + // This way we only incur the cost of the extra check once the move fails. + if (!$this->storage->get_message_headers($uid, $src_name)) { + throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE); + } throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR); } @@ -1960,7 +1966,7 @@ //If we had a collision before if (isset($this->relations$folderid$synctime . "-1")) { - return $this->relations$folderid$synctime. "-1"; + return $this->relations$folderid$synctime . "-1"; } if (!isset($this->relations$folderid$synctime)) { $rcube = rcube::get_instance();
View file
kolab-syncroton-2.4.2.tar.gz/tests/Sync/FoldersTest.php
Changed
@@ -3,16 +3,230 @@ class FoldersTest extends Tests\SyncTestCase { /** - * Test FolderSync command + * Cleanup folders */ - public function testFolderSync() + public function setUp(): void { // Note: We essentially assume the test account is in an initial state, extra folders may break tests // Anyway, we first remove folders that might have been created during tests in this file $this->deleteTestFolder('Test Folder', 'mail'); + $this->deleteTestFolder('NewFolder', 'mail'); + $this->deleteTestFolder('NewFolder2', 'mail'); $this->deleteTestFolder('Test Folder New', 'mail'); $this->deleteTestFolder('Test Contacts Folder', 'contact'); $this->deleteTestFolder('Test Contacts New', 'contact'); + parent::setUp(); + } + + /** + * Test FolderSync command + */ + public function testFolderSyncBasic() + { + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>0</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + // We expect some folders to exist (dont' know how many) + $this->assertTrue(intval($xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue) > 2); + + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>1</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + // No changes on second sync + $this->assertSame(strval(0), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); + + + //Clear the creation_synckey (that's the migration scenario) + //Shouldn't trigger a change + $rcube = \rcube::get_instance(); + $db = $rcube->get_dbh(); + $result = $db->query( + "UPDATE `syncroton_folder` SET `creation_synckey` = null", + ); + + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>1</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->printDom($dom); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + // No changes on second sync + $this->assertSame(strval(0), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); + } + + /** + * Test invalid sync key + */ + public function testFolderInvalidSyncKey() + { + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>999</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + + $this->assertSame('9', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + } + + + /** + * Test synckey reuse + */ + public function testSyncKeyResend() + { + $this->deleteTestFolder('NewFolder', 'mail'); + $this->deleteTestFolder('NewFolder2', 'mail'); + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>0</SyncKey> + </FolderSync> + EOF; + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + + //Now change something + $this->createTestFolder("NewFolder", "mail"); + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>1</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('2', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + $this->assertSame(strval(1), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); + + //Resend the same synckey + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>1</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('2', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + $this->assertSame(strval(1), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); + + //And now make sure we can still move on + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>2</SyncKey> + </FolderSync> + EOF; + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('2', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + + //Now add another folder + $this->createTestFolder("NewFolder2", "mail"); + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>2</SyncKey> + </FolderSync> + EOF; + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + $this->assertSame('3', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); + $this->assertSame(strval(1), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); + + //And finally make sure we can't go back two synckeys (because that has been cleaned up meanwhile) + $this->createTestFolder("NewFolder2", "mail"); + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>1</SyncKey> + </FolderSync> + EOF; + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->assertSame('9', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); + + // Cleanup for the other tests + $this->deleteTestFolder('NewFolder', 'mail'); + $this->deleteTestFolder('NewFolder2', 'mail'); + } + + /** + * Test FolderSync command + */ + public function testFolderSync() + { $request = <<<EOF <?xml version="1.0" encoding="utf-8"?>
View file
kolab-syncroton-2.4.2.tar.gz/tests/Sync/MoveItemsTest.php
Changed
@@ -134,14 +134,78 @@ $this->assertSame('test sync', $xpath->query("ns:Commands/ns:Add/ns:ApplicationData/Email:Subject", $root)->item(0)->nodeValue); } + public function testInvalidMove() + { + $this->emptyTestFolder('INBOX', 'mail'); + $this->emptyTestFolder('Trash', 'mail'); + $uid = $this->appendMail('INBOX', 'mail.sync1'); + $this->registerDevice(); + $inbox = array_search('INBOX', $this->folders); + $trash = array_search('Trash', $this->folders); + + // Move item that doesn't exist + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <MoveItems xmlns="uri:Move"> + <Move> + <SrcMsgId>foobar::99999</SrcMsgId> + <SrcFldId>foobar</SrcFldId> + <DstFldId>foobar</DstFldId> + </Move> + </MoveItems> + EOF; + + $response = $this->request($request, 'MoveItems'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $xpath->registerNamespace('Move', 'uri:Move'); + + $root = $xpath->query("//Move:MoveItems/Move:Response")->item(0); + $this->assertSame('1', $xpath->query("Move:Status", $root)->item(0)->nodeValue); + + // Move item that doesn't exist + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <MoveItems xmlns="uri:Move"> + <Move> + <SrcMsgId>{$inbox}::99999</SrcMsgId> + <SrcFldId>{$inbox}</SrcFldId> + <DstFldId>{$trash}</DstFldId> + </Move> + </MoveItems> + EOF; + + $response = $this->request($request, 'MoveItems'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $xpath->registerNamespace('Move', 'uri:Move'); + + $root = $xpath->query("//Move:MoveItems/Move:Response")->item(0); + $this->assertSame('1', $xpath->query("Move:Status", $root)->item(0)->nodeValue); + } + /** * Test moving a contact */ public function testMoveContact() { + if ($this->isStorageDriver('kolab')) { + // The Contacts folder is not available, and consequently appendObject fails + $this->markTestSkipped('This test only works with the DAV backend.'); + } + // Test with multi-folder support enabled self::$deviceType = 'iphone'; + // @phpstan-ignore-next-line $davFolder = $this->isStorageDriver('kolab') ? 'Contacts' : 'Addressbook'; $this->emptyTestFolder($davFolder, 'contact'); $this->deleteTestFolder($folderName = 'Test Contacts Folder', 'contact'); @@ -150,6 +214,7 @@ $this->registerDevice(); $srcFolderId = array_search($davFolder, $this->folders); + $this->assertTrue(!empty($srcFolderId)); // Create a contacts folder $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; @@ -196,6 +261,8 @@ $response = $this->request($request, 'Sync'); $this->assertEquals(200, $response->getStatusCode()); + $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); + $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $request = <<<EOF <?xml version="1.0" encoding="utf-8"?> @@ -242,6 +309,7 @@ $xpath = $this->xpath($dom); $root = $xpath->query("//ns:Sync/ns:Collections/ns:Collection")->item(0); + $this->assertSame('1', $xpath->query("ns:Status", $root)->item(0)->nodeValue); $this->assertSame($srcFolderId, $xpath->query("ns:CollectionId", $root)->item(0)->nodeValue); $this->assertSame(1, $xpath->query("ns:Commands/ns:Add", $root)->count()); $this->assertSame('Jane', $xpath->query("ns:Commands/ns:Add/ns:ApplicationData/Contacts:FirstName", $root)->item(0)->nodeValue);
View file
kolab-syncroton-2.4.2.tar.gz/tests/Sync/PingTest.php
Added
@@ -0,0 +1,102 @@ +<?php + +class PingTest extends Tests\SyncTestCase +{ + + /** + * Test Ping command + */ + public function testFolderSyncBasic() + { + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <Ping xmlns="uri:Ping"> + <HeartbeatInterval>900</HeartbeatInterval> + <Folders> + <Folder> + <Id>38b950ebd62cd9a66929c89615d0fc04</Id> + <Class>Email</Class> + </Folder> + </Folders> + </Ping> + EOF; + + $response = $this->request($request, 'Ping'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + $this->printDom($dom); + + //Initially we know no folders + $this->assertSame('7', $xpath->query("//ns:Ping/ns:Status")->item(0)->nodeValue); + + + //We discover folders with a foldersync + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <FolderSync xmlns="uri:FolderHierarchy"> + <SyncKey>0</SyncKey> + </FolderSync> + EOF; + + $response = $this->request($request, 'FolderSync'); + $this->assertEquals(200, $response->getStatusCode()); + + //Now we get to the actual ping + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <Ping xmlns="uri:Ping"> + <HeartbeatInterval>0</HeartbeatInterval> + <Folders> + <Folder> + <Id>38b950ebd62cd9a66929c89615d0fc04</Id> + <Class>Email</Class> + </Folder> + </Folders> + </Ping> + EOF; + + $response = $this->request($request, 'Ping'); + $this->assertEquals(200, $response->getStatusCode()); + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + // $this->printDom($dom); + //Initially we know no folders + $this->assertSame('2', $xpath->query("//ns:Ping/ns:Status")->item(0)->nodeValue); + } + + /** + * Test Ping command + */ + public function testUnknownFolder() + { + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <Ping xmlns="uri:Ping"> + <HeartbeatInterval>900</HeartbeatInterval> + <Folders> + <Folder> + <Id>foobar</Id> + <Class>Email</Class> + </Folder> + </Folders> + </Ping> + EOF; + + $response = $this->request($request, 'Ping'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + // $this->printDom($dom); + + $this->assertSame('7', $xpath->query("//ns:Ping/ns:Status")->item(0)->nodeValue); + } +}
View file
kolab-syncroton-2.4.2.tar.gz/tests/Sync/Sync/InconsistencyTest.php
Added
@@ -0,0 +1,146 @@ +<?php + +namespace Tests\Sync\Sync; + +class InconsistencyTest extends \Tests\SyncTestCase +{ + /** + * Test Sync command + */ + public function testSync() + { + $this->emptyTestFolder('INBOX', 'mail'); + $this->registerDevice(); + + // Append two mail messages + $uid1 = $this->appendMail('INBOX', 'mail.sync1'); + $this->appendMail('INBOX', 'mail.sync2'); + + // Initial sync + $folderId = '38b950ebd62cd9a66929c89615d0fc04'; + $syncKey = 0; + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <Sync xmlns="uri:AirSync"> + <Collections> + <Collection> + <SyncKey>{$syncKey}</SyncKey> + <CollectionId>{$folderId}</CollectionId> + </Collection> + </Collections> + </Sync> + EOF; + + $response = $this->request($request, 'Sync'); + $this->assertEquals(200, $response->getStatusCode()); + $syncKey++; + + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <Sync xmlns="uri:AirSync" xmlns:AirSyncBase="uri:AirSyncBase"> + <Collections> + <Collection> + <SyncKey>{$syncKey}</SyncKey> + <CollectionId>{$folderId}</CollectionId> + <DeletesAsMoves>1</DeletesAsMoves> + <GetChanges>1</GetChanges> + <WindowSize>2</WindowSize> + <Options> + <FilterType>0</FilterType> + <Conflict>1</Conflict> + <BodyPreference xmlns="uri:AirSyncBase"> + <Type>2</Type> + <TruncationSize>51200</TruncationSize> + <AllOrNone>0</AllOrNone> + </BodyPreference> + </Options> + </Collection> + </Collections> + </Sync> + EOF; + + $response = $this->request($request, 'Sync'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + + $root = "//ns:Sync/ns:Collections/ns:Collection"; + $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); + $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); + $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); + $this->assertSame(2, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); + + // Initial sync is complete + // We now artifically create a sync inconsistency be deleting the content part of the first mail. + // This replicates a situation that we've seen, but don't know yet how it was created in the first place. + $rcube = \rcube::get_instance(); + $db = $rcube->get_dbh(); + $result = $db->query( + "DELETE FROM `syncroton_content`" + . " WHERE `contentid` = ?", + "$folderId::$uid1" + ); + $this->assertNull($db->is_error($result)); + + // Now sync again + $request = <<<EOF + <?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> + <Sync xmlns="uri:AirSync" xmlns:AirSyncBase="uri:AirSyncBase"> + <Collections> + <Collection> + <SyncKey>{$syncKey}</SyncKey> + <CollectionId>{$folderId}</CollectionId> + <DeletesAsMoves>1</DeletesAsMoves> + <GetChanges>1</GetChanges> + <Options> + <FilterType>0</FilterType> + <Conflict>1</Conflict> + <BodyPreference xmlns="uri:AirSyncBase"> + <Type>2</Type> + <TruncationSize>51200</TruncationSize> + <AllOrNone>0</AllOrNone> + </BodyPreference> + </Options> + </Collection> + </Collections> + </Sync> + EOF; + + $response = $this->request($request, 'Sync'); + + $this->assertEquals(200, $response->getStatusCode()); + + $dom = $this->fromWbxml($response->getBody()); + $xpath = $this->xpath($dom); + + $root = "//ns:Sync/ns:Collections/ns:Collection"; + $this->assertSame('1', $xpath->query("{$root}/ns:Status")->item(0)->nodeValue); + $this->assertSame(strval(++$syncKey), $xpath->query("{$root}/ns:SyncKey")->item(0)->nodeValue); + $this->assertSame($folderId, $xpath->query("{$root}/ns:CollectionId")->item(0)->nodeValue); + $this->assertSame(1, $xpath->query("{$root}/ns:Commands/ns:Add")->count()); + + + //Assert that we have all content parts back + $sync = \kolab_sync::get_instance(); + $device = $sync->storage()->device_get(self::$deviceId); + + $result = $db->query( + "SELECT `contentid` FROM `syncroton_content`" + . " WHERE `device_id` = ?", + $device'ID' + ); + $data = ; + while ($state = $db->fetch_assoc($result)) { + $data = $state; + } + $this->assertSame(2, count($data)); + + return $syncKey; + } + +}
View file
kolab-syncroton-2.4.2.tar.gz/tests/Sync/Sync/RelationsTest.php
Changed
@@ -4,8 +4,8 @@ class RelationsTest extends \Tests\SyncTestCase { - - protected function initialSyncRequest($folderId) { + protected function initialSyncRequest($folderId) + { $request = <<<EOF <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> @@ -21,7 +21,8 @@ return $this->request($request, 'Sync'); } - protected function syncRequest($syncKey, $folderId, $windowSize = null) { + protected function syncRequest($syncKey, $folderId, $windowSize = null) + { $request = <<<EOF <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/"> @@ -77,7 +78,7 @@ $this->assertSame('Email', $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:Class")->item(0)->nodeValue); $this->assertSame($folderId, $xpath->query("//ns:Sync/ns:Collections/ns:Collection/ns:CollectionId")->item(0)->nodeValue); - // First we append + // First we append $uid1 = $this->appendMail('INBOX', 'mail.sync1'); $uid2 = $this->appendMail('INBOX', 'mail.sync2'); $this->appendMail('INBOX', 'mail.sync1', 'sync1' => 'sync3'); @@ -184,7 +185,7 @@ $this->assertSame(0, $xpath->query("{$root}/ns:ApplicationData/Email:Categories")->count()); //FIXME this currently fails because we omit the empty categories element // $this->assertSame("", $xpath->query("{$root}/ns:ApplicationData/Email:Categories")->item(0)->nodeValue); - + // Assert the db state $result = $db->query( @@ -209,4 +210,3 @@ return $syncKey; } } -
View file
kolab-syncroton-2.4.2.tar.gz/tests/SyncTestCase.php
Changed
@@ -175,9 +175,9 @@ if ($imap->folder_exists($foldername)) { // TODO exit("Not implemented for Kolab v3 storage driver"); + } else { + exit("Folder is missing"); } - - return; } $dav = $this->getDavStorage(); @@ -222,6 +222,27 @@ } /** + * Create a folder + */ + protected function createTestFolder($name, $type) + { + // Create IMAP folders + if ($type == 'mail' || $this->isStorageDriver('kolab')) { + $imap = $this->getImapStorage(); + //TODO set type if not mail + $imap->create_folder($name, true); + + $metadata = ; + $metadata'FOLDER' = ; + $metadata'FOLDER'self::$deviceId = ; + $metadata'FOLDER'self::$deviceId'S' = '1'; + $imap->set_metadata($name, '/private/vendor/kolab/activesync' => json_encode($metadata)); + + return; + } + } + + /** * Remove all objects from a folder */ protected function emptyTestFolder($name, $type) @@ -392,6 +413,14 @@ return $xpath; } + /** + * Pretty print the DOM + */ + protected function printDom($dom) + { + $dom->formatOutput = true; + print($dom->saveXML()); + } /** * adapter for phpunit < 9
View file
kolab-syncroton.dsc
Changed
@@ -2,7 +2,7 @@ Source: kolab-syncroton Binary: kolab-syncroton Architecture: all -Version: 1:2.4.2.31-1~kolab1 +Version: 1:2.4.2.34-1~kolab1 Maintainer: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> Uploaders: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> Homepage: http://www.kolab.org/
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
.