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
}
}