diff --git a/lib/kolab_sync_storage_kolab4.php b/lib/kolab_sync_storage_kolab4.php
index 2218362..9c45a89 100644
--- a/lib/kolab_sync_storage_kolab4.php
+++ b/lib/kolab_sync_storage_kolab4.php
@@ -1,571 +1,571 @@
|
| |
| 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 |
+--------------------------------------------------------------------------+
*/
/**
* Storage handling class with Kolab 4 support (IMAP + CalDAV + CardDAV)
*/
class kolab_sync_storage_kolab4 extends kolab_sync_storage
{
protected $davStorage = null;
protected $relationSupport = false;
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync_storage_kolab4 The one and only instance
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_sync_storage_kolab4();
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Class initialization
*/
public function startup()
{
$sync = kolab_sync::get_instance();
if ($sync->username === null || $sync->password === null) {
throw new Exception("Unsupported storage handler use!");
}
$url = $sync->config->get('activesync_dav_server', 'http://localhost');
if (strpos($url, '://') === false) {
$url = 'http://' . $url;
}
// Inject user+password to the URL, there's no other way to pass it to the DAV client
$url = str_replace('://', '://' . rawurlencode($sync->username) . ':' . rawurlencode($sync->password) . '@', $url);
$this->davStorage = new kolab_storage_dav($url); // DAV
$this->storage = $sync->get_storage(); // IMAP
// set additional header used by libkolab
$this->storage->set_options([
'skip_deleted' => true,
'threading' => false,
]);
// Disable paging
$this->storage->set_pagesize(999999);
}
/**
* Get list of folders available for sync
*
* @param string $deviceid Device identifier
* @param string $type Folder (class) type
* @param bool $flat_mode Enables flat-list mode
*
* @return array|bool List of mailbox folders, False on backend failure
*/
public function folders_list($deviceid, $type, $flat_mode = false)
{
$list = [];
// get mail folders subscribed for sync
if ($type === self::MODEL_EMAIL) {
$folderdata = $this->folder_meta();
if (!is_array($folderdata)) {
return false;
}
$special_folders = $this->storage->get_special_folders(true);
$type_map = [
'drafts' => 3,
'trash' => 4,
'sent' => 5,
];
// Get the folders "subscribed" for activesync
foreach ($folderdata as $folder => $meta) {
if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
|| empty($meta['FOLDER'][$deviceid]['S'])
) {
continue;
}
// Force numeric folder name to be a string (T1283)
$folder = (string) $folder;
// Activesync folder properties
$folder_data = $this->folder_data($folder, 'mail');
// Set proper type for special folders
if (($type = array_search($folder, $special_folders)) && isset($type_map[$type])) {
$folder_data['type'] = $type_map[$type];
}
$list[$folder_data['serverId']] = $folder_data;
}
} elseif (in_array($type, [self::MODEL_CONTACTS, self::MODEL_CALENDAR, self::MODEL_TASKS])) {
if (!empty($this->folders)) {
foreach ($this->folders as $unique_key => $folder) {
if (strpos($unique_key, "DAV:$type:") === 0) {
$folder_data = $this->folder_data($folder, $type);
$list[$folder_data['serverId']] = $folder_data;
}
}
}
// TODO: For now all DAV folders are subscribed
if (empty($list)) {
foreach ($this->davStorage->get_folders($type) as $folder) {
$folder_data = $this->folder_data($folder, $type);
$list[$folder_data['serverId']] = $folder_data;
// Store all folder objects in internal cache, otherwise
// Any access to the folder (or list) will invoke excessive DAV requests
$unique_key = $folder_data['serverId'] . ":$deviceid:$type";
$this->folders[$unique_key] = $folder;
}
}
}
/*
// TODO
if ($flat_mode) {
$list = $this->folders_list_flat($list, $type, $typedata);
}
*/
return $list;
}
/**
* Creates folder and subscribes to the device
*
* @param string $name Folder name (UTF8)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
* @param ?string $parentid Parent folder identifier
*
* @return string|false New folder identifier on success, False on failure
*/
public function folder_create($name, $type, $deviceid, $parentid = null)
{
// Mail folder
if ($type <= 6 || $type == 12) {
$parent = null;
$name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parentid) {
$parent = $this->folder_id2name($parentid, $deviceid);
if ($parent === null) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND);
}
}
if ($parent !== null) {
$delim = $this->storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
if ($this->storage->folder_exists($name)) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS);
}
// TODO: Support setting folder types?
$created = $this->storage->create_folder($name, true);
if ($created) {
// Set ActiveSync subscription flag
$this->folder_set($name, $deviceid, 1);
return $this->folder_id($name, 'mail');
}
// Special case when client tries to create a subfolder of INBOX
// which is not possible on Cyrus-IMAP (T2223)
if ($parent == 'INBOX' && stripos($this->last_error(), 'invalid') !== false) {
throw new Syncroton_Exception('', Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER);
}
return false;
} elseif ($type == 8 || $type == 13 || $type == 7 || $type == 15 || $type == 9 || $type == 14) {
// DAV folder
$type = preg_replace('|\..*|', '', self::type_activesync2kolab($type));
// TODO: Folder hierarchy support
// Check if folder exists
foreach ($this->davStorage->get_folders($type) as $folder) {
if ($folder->get_name() == $name) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS);
}
}
$props = ['name' => $name, 'type' => $type];
if ($id = $this->davStorage->folder_update($props)) {
return "DAV:{$type}:{$id}";
}
return false;
}
throw new \Exception("Not implemented");
}
/**
* Renames a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $new_name New folder name (UTF8)
* @param ?string $parentid Folder parent identifier
*
* @return bool True on success, False on failure
*/
public function folder_rename($folderid, $deviceid, $new_name, $parentid)
{
// DAV folder
if (strpos($folderid, 'DAV:') === 0) {
[, $type, $id] = explode(':', $folderid);
$props = [
'id' => $id,
'name' => $new_name,
'type' => $type,
];
// TODO: Folder hierarchy support
return $this->davStorage->folder_update($props) !== false;
}
// Mail folder
$old_name = $this->folder_id2name($folderid, $deviceid);
if ($parentid) {
$parent = $this->folder_id2name($parentid, $deviceid);
}
$name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if (isset($parent)) {
$delim = $this->storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
if ($name === $old_name) {
return true;
}
$this->folder_meta = null;
return $this->storage->rename_folder($old_name, $name);
}
/**
* Deletes folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
*
* @return bool True on success, False otherwise
*/
public function folder_delete($folderid, $deviceid)
{
// DAV folder
if (strpos($folderid, 'DAV:') === 0) {
[, $type, $id] = explode(':', $folderid);
return $this->davStorage->folder_delete($id, $type) !== false;
}
// Mail folder
$name = $this->folder_id2name($folderid, $deviceid);
unset($this->folder_meta[$name]);
return $this->storage->delete_folder($name);
}
/**
* Deletes contents of a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param bool $recursive Apply to the folder and its subfolders
*
* @return bool True on success, False otherwise
*/
public function folder_empty($folderid, $deviceid, $recursive = false)
{
// DAV folder
if (strpos($folderid, 'DAV:') === 0) {
[, $type, $id] = explode(':', $folderid);
if ($folder = $this->davStorage->get_folder($id, $type)) {
return $folder->delete_all();
}
// TODO: $recursive=true
return false;
}
// Mail folder
return parent::folder_empty($folderid, $deviceid, $recursive);
}
/**
* Returns folder data in Syncroton format
*/
protected function folder_data($folder, $type)
{
// Mail folders
if (strpos($type, 'mail') === 0) {
return parent::folder_data($folder, $type);
}
// DAV folders
return [
'serverId' => "DAV:{$type}:{$folder->id}",
'parentId' => 0, // TODO: Folder hierarchy
'displayName' => $folder->get_name(),
- 'type' => $this->type_kolab2activesync($type),
+ 'type' => $this->type_kolab2activesync($folder->default ? "$type.default" : $type),
];
}
/**
* Builds folder ID based on folder name
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Kolab folder type
*
* @return string|null Folder identifier (up to 64 characters)
*/
protected function folder_id($name, $type = null)
{
if (!$type) {
$type = 'mail';
}
// ActiveSync expects folder identifiers to be max.64 characters
// So we can't use just folder name
$name = (string) $name;
if ($name === '') {
return null;
}
if (strpos($type, 'mail') !== 0) {
throw new Exception("Unsupported folder_id() call on a DAV folder");
}
if (isset($this->folder_uids[$name])) {
return $this->folder_uids[$name];
}
/*
@TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves.
There's one inconvenience of this solution: folder name/type change
would be handled in ActiveSync as delete + create.
@TODO: Consider using MAILBOXID (RFC8474) that Cyrus v3 supports
// get folders unique identifier
$folderdata = $this->storage->get_metadata($name, self::UID_KEY);
if ($folderdata && !empty($folderdata[$name])) {
$uid = $folderdata[$name][self::UID_KEY];
return $this->folder_uids[$name] = $uid;
}
*/
if (strcasecmp($name, 'INBOX') === 0) {
// INBOX is always inbox, prevent from issues related with a change of
// folder type annotation (it can be initially unset).
$type = 'mail.inbox';
}
// Add type to folder UID hash, so type change can be detected by Syncroton
$uid = $name . '!!' . $type;
$uid = md5($uid);
return $this->folder_uids[$name] = $uid;
}
/**
* Returns IMAP folder name
*
* @param string $id Folder identifier
* @param string $deviceid Device dentifier
*
* @return null|string Folder name (UTF7-IMAP)
*/
public function folder_id2name($id, $deviceid)
{
// TODO: This method should become protected and be used for mail folders only
if (strpos($id, 'DAV:') === 0) {
throw new Exception("Unsupported folder_id2name() call on a DAV folder");
}
// check in cache first
if (!empty($this->folder_uids)) {
if (($name = array_search($id, $this->folder_uids)) !== false) {
return $name;
}
}
// get all folders of specified type
$folderdata = $this->folder_meta();
if (!is_array($folderdata) || empty($id)) {
return null;
}
// check if folders are "subscribed" for activesync
foreach ($folderdata as $folder => $meta) {
if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
|| empty($meta['FOLDER'][$deviceid]['S'])
) {
continue;
}
if ($uid = $this->folder_id($folder, 'mail')) {
$this->folder_uids[$folder] = $uid;
}
if ($uid === $id) {
$name = $folder;
}
}
return $name ?? null;
}
/**
* Gets kolab_storage_folder object from Activesync folder ID.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
*
* @return ?kolab_storage_folder
*/
public function getFolder($folderid, $deviceid, $type)
{
if (strpos($folderid, 'DAV:') !== 0) {
throw new Exception("Unsupported getFolder() call on a mail folder");
}
$unique_key = "$folderid:$deviceid:$type";
if (array_key_exists($unique_key, $this->folders)) {
return $this->folders[$unique_key];
}
[, $type, $id] = explode(':', $folderid);
return $this->folders[$unique_key] = $this->davStorage->get_folder($id, $type);
}
/**
* Gets Activesync preferences for a folder.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
*
* @return array Folder preferences
*/
public function getFolderConfig($folderid, $deviceid, $type)
{
// TODO: Get "alarms" from the DAV folder props, or implement
// a storage for folder properties
return [
'ALARMS' => true,
];
}
/**
* Return last storage error
*/
public static function last_error()
{
// TODO
return null;
}
/**
* Subscribe default set of folders on device registration
*/
protected function device_init_subscriptions($deviceid)
{
$config = rcube::get_instance()->config;
$mode = (int) $config->get('activesync_init_subscriptions');
$subscribed_folders = null;
// Special folders only
if (!$mode) {
$all_folders = $this->storage->get_special_folders(true);
// We do not subscribe to the Spam folder by default, same as the old Kolab driver does
unset($all_folders['junk']);
$all_folders = array_unique(array_merge(['INBOX'], array_values($all_folders)));
}
// other modes
elseif (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) {
$all_folders = $this->storage->list_folders();
if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) {
$subscribed_folders = $this->storage->list_folders_subscribed();
}
} else {
$all_folders = $this->storage->list_folders_subscribed();
}
foreach ($all_folders as $folder) {
$ns = strtoupper($this->storage->folder_namespace($folder));
// subscribe the folder according to configured mode
// and folder namespace/subscription status
if (!$mode
|| ($mode & constant("self::INIT_ALL_{$ns}"))
|| (($mode & constant("self::INIT_SUB_{$ns}")) && ($subscribed_folders === null || in_array($folder, $subscribed_folders)))
) {
$this->folder_set($folder, $deviceid, 1);
}
}
// TODO: Subscribe personal DAV folders, for now we assume all are subscribed
// TODO: Subscribe shared DAV folders
}
public function getExtraData($folderid, $deviceid)
{
if (strpos($folderid, 'DAV:') === 0) {
return null;
}
return parent::getExtraData($folderid, $deviceid);
}
}
diff --git a/tests/Sync/FoldersTest.php b/tests/Sync/FoldersTest.php
index 5457154..ed234c5 100644
--- a/tests/Sync/FoldersTest.php
+++ b/tests/Sync/FoldersTest.php
@@ -1,389 +1,389 @@
deleteTestFolder('Test Folder', 'mail');
$this->deleteTestFolder('Test Folder New', 'mail');
$this->deleteTestFolder('Test Contacts Folder', 'contact');
$this->deleteTestFolder('Test Contacts New', 'contact');
$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_USER_CREATED],
+ ['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_USER_CREATED],
+ ['/^(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_USER_CREATED],
+ ['/^(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
}
}