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