diff --git a/docs/SQL/mysql.initial.sql b/docs/SQL/mysql.initial.sql index ac433c8..384d009 100644 --- a/docs/SQL/mysql.initial.sql +++ b/docs/SQL/mysql.initial.sql @@ -1,118 +1,119 @@ CREATE TABLE IF NOT EXISTS `syncroton_policy` ( `id` varchar(40) NOT NULL, `name` varchar(255) NOT NULL, `description` varchar(255) DEFAULT NULL, `policy_key` varchar(64) NOT NULL, `json_policy` blob NOT NULL, PRIMARY KEY (`id`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_device` ( `id` varchar(40) NOT NULL, `deviceid` varchar(64) NOT NULL, `devicetype` varchar(64) NOT NULL, `owner_id` varchar(40) NOT NULL, `acsversion` varchar(40) NOT NULL, `policykey` varchar(64) DEFAULT NULL, `policy_id` varchar(40) DEFAULT NULL, `useragent` varchar(255) DEFAULT NULL, `imei` varchar(255) DEFAULT NULL, `model` varchar(255) DEFAULT NULL, `friendlyname` varchar(255) DEFAULT NULL, `os` varchar(255) DEFAULT NULL, `oslanguage` varchar(255) DEFAULT NULL, `phonenumber` varchar(255) DEFAULT NULL, `pinglifetime` int(11) DEFAULT NULL, `remotewipe` int(11) DEFAULT '0', `pingfolder` longblob, `lastsynccollection` longblob DEFAULT NULL, `lastping` datetime DEFAULT NULL, `contactsfilter_id` varchar(40) DEFAULT NULL, `calendarfilter_id` varchar(40) DEFAULT NULL, `tasksfilter_id` varchar(40) DEFAULT NULL, `emailfilter_id` varchar(40) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `owner_id--deviceid` (`owner_id`, `deviceid`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_folder` ( `id` varchar(40) NOT NULL, `device_id` varchar(40) NOT NULL, `class` varchar(64) NOT NULL, `folderid` varchar(254) NOT NULL, `parentid` varchar(254) DEFAULT NULL, `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`), UNIQUE KEY `device_id--class--folderid` (`device_id`(40),`class`(40),`folderid`(40)), KEY `folderstates::device_id--devices::id` (`device_id`), CONSTRAINT `folderstates::device_id--devices::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_synckey` ( `id` varchar(40) NOT NULL, `device_id` varchar(40) NOT NULL DEFAULT '', `type` varchar(64) NOT NULL DEFAULT '', `counter` int(11) NOT NULL DEFAULT '0', `lastsync` datetime DEFAULT NULL, `pendingdata` longblob, `client_id_map` longblob DEFAULT NULL, `extra_data` longblob DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `device_id--type--counter` (`device_id`,`type`,`counter`), CONSTRAINT `syncroton_synckey::device_id--syncroton_device::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_content` ( `id` varchar(40) NOT NULL, `device_id` varchar(40) NOT NULL, `folder_id` varchar(40) NOT NULL, `contentid` varchar(128) NOT NULL, `creation_time` datetime DEFAULT NULL, `creation_synckey` int(11) NOT NULL, `is_deleted` tinyint(1) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `device_id--folder_id--contentid` (`device_id`(40),`folder_id`(40),`contentid`(128)), KEY `syncroton_contents::device_id` (`device_id`), CONSTRAINT `syncroton_contents::device_id--syncroton_device::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_data` ( `id` varchar(40) NOT NULL, `class` varchar(40) NOT NULL, `folder_id` varchar(40) NOT NULL, `data` longblob, PRIMARY KEY (`id`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_data_folder` ( `id` varchar(40) NOT NULL, `type` int(11) NOT NULL, `name` varchar(255) NOT NULL, `owner_id` varchar(40) NOT NULL, `parent_id` varchar(40) DEFAULT NULL, PRIMARY KEY (`id`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; CREATE TABLE IF NOT EXISTS `syncroton_relations_state` ( `device_id` varchar(40) NOT NULL, `folder_id` varchar(40) NOT NULL, `synctime` datetime NOT NULL, `data` longblob, PRIMARY KEY (`device_id`,`folder_id`,`synctime`), KEY `syncroton_relations_state::device_id` (`device_id`), CONSTRAINT `syncroton_relations_state::device_id--syncroton_device::id` FOREIGN KEY (`device_id`) REFERENCES `syncroton_device` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; -- Roundcube core table should exist if we're using the same database CREATE TABLE IF NOT EXISTS `system` ( `name` varchar(64) NOT NULL, `value` mediumtext, 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'); diff --git a/docs/SQL/mysql/2024102300.sql b/docs/SQL/mysql/2024102300.sql new file mode 100644 index 0000000..692f045 --- /dev/null +++ b/docs/SQL/mysql/2024102300.sql @@ -0,0 +1 @@ +ALTER TABLE `syncroton_folder` ADD `creation_synckey` int(11) NOT NULL DEFAULT '0'; diff --git a/lib/ext/Syncroton/Backend/IFolder.php b/lib/ext/Syncroton/Backend/IFolder.php index d6fd1b8..c829070 100755 --- a/lib/ext/Syncroton/Backend/IFolder.php +++ b/lib/ext/Syncroton/Backend/IFolder.php @@ -1,54 +1,55 @@ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * sql backend class for the folder state * * @package Syncroton * @subpackage Backend */ interface Syncroton_Backend_IFolder extends Syncroton_Backend_IBackend { /** * get folder indentified by $folderId * * @param Syncroton_Model_Device|string $deviceId * @param string $folderId * @return Syncroton_Model_IFolder */ public function getFolder($deviceId, $folderId); /** * get array of ids which got send to the client for a given class * * @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 * * @param Syncroton_Model_Device|string $deviceId */ public function resetState($deviceId); /** * Find out if the folder hierarchy changed since the last FolderSync * * @param Syncroton_Model_Device $device Device object * * @return bool True if folders hierarchy changed, False otherwise */ public function hasHierarchyChanges($device); } diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php index a632142..908aec9 100644 --- a/lib/ext/Syncroton/Command/FolderCreate.php +++ b/lib/ext/Syncroton/Command/FolderCreate.php @@ -1,162 +1,163 @@ */ /** * class to handle ActiveSync FolderCreate command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml { protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderCreate'; /** * @var Syncroton_Model_IFolder */ protected $_folder; /** * @var int */ protected $_status; /** * parse FolderCreate request */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); $syncKey = (int)$xml->SyncKey; if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey"); } if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed."); } $this->_status = Syncroton_Command_FolderSync::STATUS_INVALID_SYNC_KEY; return; } $folder = new Syncroton_Model_Folder($xml); if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " parentId: {$folder->parentId} displayName: {$folder->displayName}"); } if (!strlen($folder->displayName)) { $this->_status = Syncroton_Command_FolderSync::STATUS_MISFORMATTED; return; } switch($folder->type) { case Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_CALENDAR; break; case Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_CONTACTS; break; case Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_EMAIL; break; case Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_NOTES; break; case Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED: $folder->class = Syncroton_Data_Factory::CLASS_TASKS; break; default: // unsupported type return; } try { $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); $this->_folder = $dataController->createFolder($folder); if (!$this->_folder) { $this->_status = Syncroton_Command_FolderSync::STATUS_UNKNOWN_ERROR; } else { $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 { $this->_folderBackend->getFolder($this->_device, $this->_folder->serverId); if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " Attempted to create a folder that already exists. parentId: {$folder->parentId} displayName: {$folder->displayName}"); } // The folder already exists $this->_status = Syncroton_Command_FolderSync::STATUS_FOLDER_EXISTS; } catch (Syncroton_Exception_NotFound $e) { // This is the normal case if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); } $this->_folderBackend->create($this->_folder); } } } catch (Syncroton_Exception_Status $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); } $this->_status = $e->getCode(); } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); } $this->_status = Syncroton_Command_FolderSync::STATUS_UNKNOWN_ERROR; } } /** * generate FolderCreate response */ public function getResponse() { $folderCreate = $this->_outputDom->documentElement; if ($this->_status) { $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', $this->_status)); } else { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; // store folder in state backend $this->_syncStateBackend->update($this->_syncState); // create xml output $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncroton_Command_FolderSync::STATUS_SUCCESS)); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $this->_folder->serverId)); } return $this->_outputDom; } } diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php index f5892c5..78a7cec 100644 --- a/lib/ext/Syncroton/Command/FolderSync.php +++ b/lib/ext/Syncroton/Command/FolderSync.php @@ -1,298 +1,307 @@ */ /** * class to handle ActiveSync FolderSync command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml { public const STATUS_SUCCESS = 1; public const STATUS_FOLDER_EXISTS = 2; public const STATUS_IS_SPECIAL_FOLDER = 3; public const STATUS_FOLDER_NOT_FOUND = 4; public const STATUS_PARENT_FOLDER_NOT_FOUND = 5; public const STATUS_SERVER_ERROR = 6; public const STATUS_ACCESS_DENIED = 7; public const STATUS_REQUEST_TIMED_OUT = 8; public const STATUS_INVALID_SYNC_KEY = 9; public const STATUS_MISFORMATTED = 10; public const STATUS_UNKNOWN_ERROR = 11; /** * some usefull constants for working with the xml files */ public const FOLDERTYPE_GENERIC_USER_CREATED = 1; public const FOLDERTYPE_INBOX = 2; public const FOLDERTYPE_DRAFTS = 3; public const FOLDERTYPE_DELETEDITEMS = 4; public const FOLDERTYPE_SENTMAIL = 5; public const FOLDERTYPE_OUTBOX = 6; public const FOLDERTYPE_TASK = 7; public const FOLDERTYPE_CALENDAR = 8; public const FOLDERTYPE_CONTACT = 9; public const FOLDERTYPE_NOTE = 10; public const FOLDERTYPE_JOURNAL = 11; public const FOLDERTYPE_MAIL_USER_CREATED = 12; public const FOLDERTYPE_CALENDAR_USER_CREATED = 13; public const FOLDERTYPE_CONTACT_USER_CREATED = 14; public const FOLDERTYPE_TASK_USER_CREATED = 15; public const FOLDERTYPE_JOURNAL_USER_CREATED = 16; public const FOLDERTYPE_NOTE_USER_CREATED = 17; public const FOLDERTYPE_UNKOWN = 18; protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderSync'; protected $_classes = [ Syncroton_Data_Factory::CLASS_CALENDAR, Syncroton_Data_Factory::CLASS_CONTACTS, Syncroton_Data_Factory::CLASS_EMAIL, Syncroton_Data_Factory::CLASS_NOTES, Syncroton_Data_Factory::CLASS_TASKS, ]; /** * @var string */ protected $_syncKey; /** * Parse FolderSync request */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); $syncKey = (int)$xml->SyncKey; if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey"); } if ($syncKey === 0) { $this->_syncState = new Syncroton_Model_SyncState([ 'device_id' => $this->_device, 'counter' => 0, 'type' => 'FolderSync', 'lastsync' => $this->_syncTimeStamp, ]); // reset state of foldersync $this->_syncStateBackend->resetState($this->_device, 'FolderSync'); return; } 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'); } } /** * generate FolderSync response * * @todo changes are missing in response (folder got renamed for example) */ public function getResponse() { $folderSync = $this->_outputDom->documentElement; // invalid synckey provided if (!$this->_syncState instanceof Syncroton_Model_SyncState) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed."); } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY)); return $this->_outputDom; } // send headers from options command also when FolderSync SyncKey is 0 if ($this->_syncState->counter == 0) { $optionsCommand = new Syncroton_Command_Options(); $this->_headers = array_merge($this->_headers, $optionsCommand->getHeaders()); } $adds = []; $updates = []; $deletes = []; foreach($this->_classes as $class) { try { $dataController = Syncroton_Data_Factory::factory($class, $this->_device, $this->_syncTimeStamp); } catch (Exception $e) { // backend not defined if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " no data backend defined for class: " . $class); } continue; } try { // retrieve all folders available in data backend $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 $changedFolders = $dataController->getChangedFolders($this->_syncState->lastsync, $this->_syncTimeStamp); } else { $changedFolders = []; } // only folders which were sent to the client already are allowed to be in $changedFolders $changedFolders = array_intersect_key($changedFolders, $clientFolders); } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getMessage()); } if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getTraceAsString()); } // The Status element is global for all collections. If one collection fails, // a failure status MUST be returned for all collections. if ($e instanceof Syncroton_Exception_Status) { $status = $e->getCode(); } else { $status = Syncroton_Exception_Status_FolderSync::UNKNOWN_ERROR; } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', $status)); return $this->_outputDom; } $serverFoldersIds = array_keys($serverFolders); // is this the first sync? if ($this->_syncState->counter == 0) { $clientFoldersIds = []; } else { $clientFoldersIds = array_keys($clientFolders); } // calculate added entries $serverDiff = array_diff($serverFoldersIds, $clientFoldersIds); foreach ($serverDiff as $serverFolderId) { // have we created a folderObject in syncroton_folder before? if (isset($clientFolders[$serverFolderId])) { $add = $clientFolders[$serverFolderId]; } else { $add = $serverFolders[$serverFolderId]; $add->creationTime = $this->_syncTimeStamp; + $add->creationSynckey = $this->_syncState->counter + 1; $add->deviceId = $this->_device->id; unset($add->id); } $add->class = $class; $adds[] = $add; } // calculate changed entries foreach ($changedFolders as $changedFolder) { $change = $clientFolders[$changedFolder->serverId]; $change->displayName = $changedFolder->displayName; $change->parentId = $changedFolder->parentId; $change->type = $changedFolder->type; $updates[] = $change; } // Find changes in case backend does not support folder changes detection. // On some backends getChangedFolders() can return an empty result. // We make sure all is up-to-date comparing folder properties. foreach ($clientFoldersIds as $folderId) { if (isset($serverFolders[$folderId])) { $c = $clientFolders[$folderId]; $s = $serverFolders[$folderId]; if ($c->displayName !== $s->displayName || strval($c->parentId) !== strval($s->parentId) || $c->type != $s->type ) { $c->displayName = $s->displayName; $c->parentId = $s->parentId; $c->type = $s->type; $updates[] = $c; } } } // 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) { $deletes[] = $clientFolders[$serverFolderId]; } } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS)); $count = count($adds) + count($updates) + count($deletes); if($count > 0) { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; } // create xml output $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); $changes = $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Changes')); $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Count', $count)); foreach($adds as $folder) { $add = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Add')); $folder->appendXML($add, $this->_device); // 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); + } } } foreach($updates as $folder) { $update = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Update')); $folder->appendXML($update, $this->_device); $this->_folderBackend->update($folder); } foreach($deletes as $folder) { $delete = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Delete')); $delete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder->serverId)); $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; } } diff --git a/lib/ext/Syncroton/Model/Folder.php b/lib/ext/Syncroton/Model/Folder.php index 86e1ba6..29ac358 100644 --- a/lib/ext/Syncroton/Model/Folder.php +++ b/lib/ext/Syncroton/Model/Folder.php @@ -1,40 +1,41 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model */ class Syncroton_Model_Folder extends Syncroton_Model_AXMLEntry implements Syncroton_Model_IFolder { protected $_xmlBaseElement = ['FolderUpdate', 'FolderCreate']; protected $_properties = [ 'FolderHierarchy' => [ 'parentId' => ['type' => 'string'], 'serverId' => ['type' => 'string'], 'displayName' => ['type' => 'string'], 'type' => ['type' => 'number'], ], 'Internal' => [ 'id' => ['type' => 'string'], 'deviceId' => ['type' => 'string'], 'ownerId' => ['type' => 'string'], 'class' => ['type' => 'string'], 'creationTime' => ['type' => 'datetime'], + 'creationSynckey' => ['type' => 'number'], 'lastfiltertype' => ['type' => 'number'], 'resync' => ['type' => 'number'], ], ]; } diff --git a/lib/ext/Syncroton/Model/IFolder.php b/lib/ext/Syncroton/Model/IFolder.php index 39aee4c..0cc4a45 100644 --- a/lib/ext/Syncroton/Model/IFolder.php +++ b/lib/ext/Syncroton/Model/IFolder.php @@ -1,29 +1,30 @@ */ /** * class to handle ActiveSync Sync command * * @package Syncroton * @subpackage Model * @property string $id * @property string $deviceId * @property string $class * @property string $serverId * @property string $parentId * @property string $displayName * @property DateTime $creationTime + * @property int $creationSynckey * @property int $lastfiltertype * @property int $type */ interface Syncroton_Model_IFolder { } diff --git a/lib/kolab_sync_backend_folder.php b/lib/kolab_sync_backend_folder.php index 4dbe899..adcf3bd 100644 --- a/lib/kolab_sync_backend_folder.php +++ b/lib/kolab_sync_backend_folder.php @@ -1,195 +1,199 @@ | | | | 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 | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Kolab backend class for the folder state storage */ class kolab_sync_backend_folder extends kolab_sync_backend_common implements Syncroton_Backend_IFolder { protected $table_name = 'syncroton_folder'; protected $interface_name = 'Syncroton_Model_IFolder'; /** * Delete all stored folder ids for a given device * * @param Syncroton_Model_Device|string $deviceid Device object or identifier */ public function resetState($deviceid) { $device_id = $deviceid instanceof Syncroton_Model_IDevice ? $deviceid->id : $deviceid; $where[] = $this->db->quote_identifier('device_id') . ' = ' . $this->db->quote($device_id); $this->db->query('DELETE FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); } /** * Get array of ids which got send to the client for a given class * * @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 = []; while ($folder = $this->db->fetch_assoc($select)) { $result[$folder['folderid']] = $this->get_object($folder); } return $result; } /** * Get folder * * @param Syncroton_Model_Device|string $deviceid Device object or identifier * @param string $folderid Folder identifier * * @return Syncroton_Model_IFolder Folder object */ public function getFolder($deviceid, $folderid) { $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('folderid') . ' = ' . $this->db->quote($folderid); $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); $folder = $this->db->fetch_assoc($select); if (empty($folder) || !empty($folder['resync'])) { throw new Syncroton_Exception_NotFound('Folder not found'); } return $this->get_object($folder); } /** * Find out if the folder hierarchy changed since the last FolderSync * * @param Syncroton_Model_Device $device Device object * * @return bool True if folders hierarchy changed, False otherwise */ public function hasHierarchyChanges($device) { $timestamp = new DateTime('now', new DateTimeZone('utc')); $client_crc = ''; $server_crc = ''; $client_folders = []; $server_folders = []; $folder_classes = [ Syncroton_Data_Factory::CLASS_CALENDAR, Syncroton_Data_Factory::CLASS_CONTACTS, Syncroton_Data_Factory::CLASS_EMAIL, Syncroton_Data_Factory::CLASS_NOTES, Syncroton_Data_Factory::CLASS_TASKS, ]; // 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 $dataController = Syncroton_Data_Factory::factory($class, $device, $timestamp); $server_folders = array_merge($server_folders, $dataController->getAllFolders()); } catch (Exception $e) { rcube::raise_error($e, true, false); // This is server error, returning True might cause infinite sync loops return false; } } ksort($client_folders); ksort($server_folders); foreach ($client_folders as $folder) { $client_crc .= '^' . $folder->serverId . ':' . $folder->displayName . ':' . $folder->parentId; } foreach ($server_folders as $folder) { $server_crc .= '^' . $folder->serverId . ':' . $folder->displayName . ':' . $folder->parentId; } return $client_crc !== $server_crc; } /** * (non-PHPdoc) * @see kolab_sync_backend_common::from_camelcase() */ protected function from_camelcase($string) { switch ($string) { case 'displayName': case 'parentId': return strtolower($string); case 'serverId': return 'folderid'; default: return parent::from_camelcase($string); } } /** * (non-PHPdoc) * @see kolab_sync_backend_common::to_camelcase() */ protected function to_camelcase($string, $ucFirst = true) { switch ($string) { case 'displayname': return 'displayName'; case 'parentid': return 'parentId'; case 'folderid': return 'serverId'; default: return parent::to_camelcase($string, $ucFirst); } } } diff --git a/tests/Sync/FoldersTest.php b/tests/Sync/FoldersTest.php index 9d38cda..836fbfb 100644 --- a/tests/Sync/FoldersTest.php +++ b/tests/Sync/FoldersTest.php @@ -1,533 +1,603 @@ 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 = << 0 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 = << 1 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 = << + + + 1 + + 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 = << 999 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 = << 0 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 = << 1 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 = << 1 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 = << + + + 2 + + 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 = << + + + 2 + + 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); - $this->assertSame(strval(0), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->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 = << + + + 1 + + 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 = << 0 EOF; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // Note: We're expecting activesync_init_subscriptions=0 here. if ($this->isStorageDriver('kolab4')) { $folders = [ ['Calendar', Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR], ['Contacts', Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT], ['INBOX', Syncroton_Command_FolderSync::FOLDERTYPE_INBOX], ['Drafts', Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS], ['Sent', Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL], ['Trash', Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS], ['Tasks', Syncroton_Command_FolderSync::FOLDERTYPE_TASK], ]; } else { $folders = [ ['Calendar', Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR], ['Contacts', Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT], ['INBOX', Syncroton_Command_FolderSync::FOLDERTYPE_INBOX], ['Drafts', Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS], ['Sent', Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL], ['Trash', Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS], ['Notes', Syncroton_Command_FolderSync::FOLDERTYPE_NOTE], ['Tasks', Syncroton_Command_FolderSync::FOLDERTYPE_TASK], ]; } $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(strval(count($folders)), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); foreach ($folders as $idx => $folder) { $this->assertSame($folder[0], $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item($idx)->nodeValue); $this->assertSame((string) $folder[1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item($idx)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item($idx)->nodeValue); } // Test with multi-folder support enabled self::$deviceType = 'iphone'; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); if ($this->isStorageDriver('kolab4')) { $folders = [ ['Calendar', Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR], // Note: Kolab 4 with Cyrus DAV uses Addressbook, but Kolab 3 with iRony would use 'Contacts' ['/^(Contacts|Addressbook)$/', Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT], ['INBOX', Syncroton_Command_FolderSync::FOLDERTYPE_INBOX], ['Drafts', Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS], ['Sent', Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL], ['Trash', Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS], // Note: For now Kolab 4 uses the same Calendar folder for calendar and tasks ['/^(Tasks|Calendar)$/', Syncroton_Command_FolderSync::FOLDERTYPE_TASK], ]; } $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(strval(count($folders)), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); foreach ($folders as $idx => $folder) { $displayName = $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item($idx)->nodeValue; if (str_starts_with($folder[0], '/')) { $this->assertMatchesRegularExpression($folder[0], $displayName); } else { $this->assertSame($folder[0], $displayName); } $this->assertSame((string) $folder[1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item($idx)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item($idx)->nodeValue); $idx++; } // After we switched to multi-folder supported mode we expect next FolderSync // to delete the old "collective" folders $request = << 1 EOF; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $deleted = $this->isStorageDriver('kolab4') ? 3 : 4; // No Notes folder in Kolab4 $syncKey = 2; $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame(strval($syncKey), $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(strval($deleted), $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); $this->assertSame($deleted, $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete")->length); return $syncKey; } /** * Test FolderCreate command * * @depends testFolderSync */ public function testFolderCreate($syncKey) { // Multi-folder mode self::$deviceType = 'iphone'; // Create a mail folder $folderName1 = 'Test Folder'; $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED; $request = << {$syncKey} 0 {$folderName1} {$folderType} EOF; $response = $this->request($request, 'FolderCreate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderCreate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:FolderCreate/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("//ns:FolderCreate/ns:ServerId")->count()); $folder1 = $xpath->query("//ns:FolderCreate/ns:ServerId")->item(0)->nodeValue; // Note: After FolderCreate there are no changes in the following FolderSync expected // Create a contacts folder $folderName2 = 'Test Contacts Folder'; $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; $request = << {$syncKey} 0 {$folderName2} {$folderType} EOF; $response = $this->request($request, 'FolderCreate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderCreate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$syncKey), $xpath->query("//ns:FolderCreate/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("//ns:FolderCreate/ns:ServerId")->count()); $folder2 = $xpath->query("//ns:FolderCreate/ns:ServerId")->item(0)->nodeValue; // Note: After FolderCreate there are no changes in the following FolderSync expected // TODO: Test folder with a parent return [ 'SyncKey' => $syncKey, 'folders' => [ $folder1, $folder2, ], ]; } /** * Test FolderUpdate command * * @depends testFolderCreate */ public function testFolderUpdate($params) { // Multi-folder mode self::$deviceType = 'iphone'; // Test renaming a mail folder $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED; $request = << {$params['SyncKey']} {$params['folders'][0]} Test Folder New {$folderType} EOF; $response = $this->request($request, 'FolderUpdate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderUpdate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderUpdate/ns:SyncKey")->item(0)->nodeValue); // Test FolderSync after folder update, get the new folder id (for delete test) $request = << {$params['SyncKey']} EOF; $response = $this->request($request, 'FolderSync'); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); // Note we expect Add+Delete here, instead of Update (but this could change in the future) $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); $this->assertSame(1, $xpath->query("//ns:FolderSync/ns:Changes/ns:Add")->length); $this->assertSame(1, $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete")->length); $this->assertSame($params['folders'][0], $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete/ns:ServerId")->item(0)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item(0)->nodeValue); $this->assertSame('Test Folder New', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item(0)->nodeValue); $this->assertSame(strval($folderType), $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item(0)->nodeValue); $params['folders'][0] = $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ServerId")->item(0)->nodeValue; // Test renaming a contacts folder $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED; $request = << {$params['SyncKey']} {$params['folders'][1]} Test Contacts New {$folderType} EOF; $response = $this->request($request, 'FolderUpdate'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderUpdate/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderUpdate/ns:SyncKey")->item(0)->nodeValue); // Test FolderSync after folder update, get the new folder id (for delete test) $request = << {$params['SyncKey']} EOF; $response = $this->request($request, 'FolderSync'); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderSync/ns:SyncKey")->item(0)->nodeValue); if ($this->isStorageDriver('kolab4')) { // Note we expect Update here, not Add+Delete, folder ID does not change $this->assertSame('1', $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); $this->assertSame($params['folders'][1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Update/ns:ServerId")->item(0)->nodeValue); $this->assertSame('Test Contacts New', $xpath->query("//ns:FolderSync/ns:Changes/ns:Update/ns:DisplayName")->item(0)->nodeValue); $this->assertSame(strval($folderType), $xpath->query("//ns:FolderSync/ns:Changes/ns:Update/ns:Type")->item(0)->nodeValue); } else { // Note we expect Add+Delete here, instead of Update (but this could change in the future) $this->assertSame('2', $xpath->query("//ns:FolderSync/ns:Changes/ns:Count")->item(0)->nodeValue); $this->assertSame($params['folders'][1], $xpath->query("//ns:FolderSync/ns:Changes/ns:Delete/ns:ServerId")->item(0)->nodeValue); $this->assertSame('0', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ParentId")->item(0)->nodeValue); $this->assertSame('Test Contacts New', $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:DisplayName")->item(0)->nodeValue); $this->assertSame(strval($folderType), $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:Type")->item(0)->nodeValue); $params['folders'][1] = $xpath->query("//ns:FolderSync/ns:Changes/ns:Add/ns:ServerId")->item(0)->nodeValue; } // TODO: Test folder with a parent change // TODO: Assert the folder name has changed in the storage // TODO: Test Sync after a DAV folder rename made in another client return $params; } /** * Test FolderDelete command * * @depends testFolderUpdate */ public function testFolderDelete($params) { // Multi-folder mode self::$deviceType = 'iphone'; // Delete mail folder $request = << {$params['SyncKey']} {$params['folders'][0]} EOF; $response = $this->request($request, 'FolderDelete'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderDelete/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderDelete/ns:SyncKey")->item(0)->nodeValue); // Note: After FolderDelete there are no changes in the following FolderSync expected // Delete contacts folder $request = << {$params['SyncKey']} {$params['folders'][1]} EOF; $response = $this->request($request, 'FolderDelete'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); $this->assertSame('1', $xpath->query("//ns:FolderDelete/ns:Status")->item(0)->nodeValue); $this->assertSame(strval(++$params['SyncKey']), $xpath->query("//ns:FolderDelete/ns:SyncKey")->item(0)->nodeValue); // Note: After FolderDelete there are no changes in the following FolderSync expected // TODO: Assert the folders no longer exist } }