diff --git a/tests/Sync/FoldersTest.php b/tests/Sync/FoldersTest.php index 91cf0f0..9d38cda 100644 --- a/tests/Sync/FoldersTest.php +++ b/tests/Sync/FoldersTest.php @@ -1,467 +1,533 @@ deleteTestFolder('Test Folder', 'mail'); + $this->deleteTestFolder('NewFolder', '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); } /** * 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'); + $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('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); + } + /** * 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 } } diff --git a/tests/SyncTestCase.php b/tests/SyncTestCase.php index 36eb269..0a8209c 100644 --- a/tests/SyncTestCase.php +++ b/tests/SyncTestCase.php @@ -1,407 +1,428 @@ markTestSkipped('Not setup'); } self::$deviceType = null; } /** * {@inheritDoc} */ public static function setUpBeforeClass(): void { $sync = \kolab_sync::get_instance(); $config = $sync->config; $db = $sync->get_dbh(); self::$username = $config->get('activesync_test_username'); self::$password = $config->get('activesync_test_password'); self::$host = $config->get('activesync_test_host', 'http://localhost:8000'); if (empty(self::$username)) { return; } self::$deviceId = 'test' . time(); $db->query('DELETE FROM syncroton_device'); $db->query('DELETE FROM syncroton_synckey'); $db->query('DELETE FROM syncroton_folder'); $db->query('DELETE FROM syncroton_data'); $db->query('DELETE FROM syncroton_data_folder'); $db->query('DELETE FROM syncroton_content'); $db->query('DELETE FROM syncroton_relations_state'); self::$client = new \GuzzleHttp\Client([ 'http_errors' => false, 'base_uri' => self::$host, 'verify' => false, 'auth' => [self::$username, self::$password], 'connect_timeout' => 10, 'timeout' => 10, 'headers' => [ 'Content-Type' => 'application/xml; charset=utf-8', 'Depth' => '1', ], ]); // TODO: execute: php -S localhost:8000 } /** * {@inheritDoc} */ public static function tearDownAfterClass(): void { if (self::$deviceId) { $sync = \kolab_sync::get_instance(); if (self::$authenticated || $sync->authenticate(self::$username, self::$password)) { $sync->password = self::$password; $storage = $sync->storage(); $storage->device_delete(self::$deviceId); } $db = $sync->get_dbh(); $db->query('DELETE FROM syncroton_device'); $db->query('DELETE FROM syncroton_synckey'); $db->query('DELETE FROM syncroton_folder'); $db->query('DELETE FROM syncroton_relations_state'); } } /** * Append an email message to the IMAP folder */ protected function appendMail($folder, $filename, $replace = []) { $imap = $this->getImapStorage(); $source = __DIR__ . '/src/' . $filename; if (!file_exists($source)) { exit("File does not exist: {$source}"); } $is_file = true; if (!empty($replace)) { $is_file = false; $source = file_get_contents($source); foreach ($replace as $token => $value) { $source = str_replace($token, $value, $source); } } $uid = $imap->save_message($folder, $source, '', $is_file); if ($uid === false) { exit("Failed to append mail {$filename} into {$folder}"); } return $uid; } /** * Run A SQL query */ protected function runSQLQuery($query) { $sync = \kolab_sync::get_instance(); $db = $sync->get_dbh(); $db->query($query); } /** * Mark an email message as read over IMAP */ protected function markMailAsRead($folder, $uids) { $imap = $this->getImapStorage(); return $imap->set_flag($uids, 'SEEN', $folder); } /** * List emails over IMAP */ protected function listEmails($folder, $uids) { $imap = $this->getImapStorage(); return $imap->list_flags($folder, $uids); } /** * Append an DAV object to a DAV/IMAP folder */ protected function appendObject($foldername, $filename, $type) { $path = __DIR__ . '/src/' . $filename; if (!file_exists($path)) { exit("File does not exist: {$path}"); } $content = file_get_contents($path); $uid = preg_match('/UID:(?:urn:uuid:)?([a-z0-9-]+)/', $content, $m) ? $m[1] : null; if (empty($uid)) { exit("Filed to find UID in {$path}"); } if ($this->isStorageDriver('kolab')) { $imap = $this->getImapStorage(); if ($imap->folder_exists($foldername)) { // TODO exit("Not implemented for Kolab v3 storage driver"); } else { exit("Folder is missing"); } } $dav = $this->getDavStorage(); foreach ($dav->get_folders($type) as $folder) { if ($folder->get_name() === $foldername) { $dav_type = $folder->get_dav_type(); $location = $folder->object_location($uid); if ($folder->dav->create($location, $content, $dav_type) !== false) { return; } } } exit("Failed to append object into {$foldername}"); } /** * Delete a folder */ protected function deleteTestFolder($name, $type) { // Deleting IMAP folders if ($type == 'mail' || $this->isStorageDriver('kolab')) { $imap = $this->getImapStorage(); if ($imap->folder_exists($name)) { $imap->delete_folder($name); } return; } // Deleting DAV folders $dav = $this->getDavStorage(); foreach ($dav->get_folders($type) as $folder) { if ($folder->get_name() === $name) { $dav->folder_delete($folder->id, $type); } } } + /** + * Create a folder + */ + protected function createTestFolder($name, $type) + { + // Create IMAP folders + if ($type == 'mail' || $this->isStorageDriver('kolab')) { + $imap = $this->getImapStorage(); + //TODO set type if not mail + $imap->create_folder($name, true); + + $metadata = []; + $metadata['FOLDER'] = []; + $metadata['FOLDER'][self::$deviceId] = []; + $metadata['FOLDER'][self::$deviceId]['S'] = '1'; + $imap->set_metadata($name, ['/private/vendor/kolab/activesync' => json_encode($metadata)]); + + return; + } + } + /** * Remove all objects from a folder */ protected function emptyTestFolder($name, $type) { // Deleting in IMAP folders if ($type == 'mail' || $this->isStorageDriver('kolab')) { $imap = $this->getImapStorage(); $imap->delete_message('*', $name); return; } // Deleting in DAV folders $dav = $this->getDavStorage(); foreach ($dav->get_folders($type) as $folder) { if ($folder->get_name() === $name) { $folder->delete_all(); } } } /** * Convert WBXML binary content into XML */ protected function fromWbxml($binary) { $stream = fopen('php://memory', 'r+'); fwrite($stream, $binary); rewind($stream); $decoder = new \Syncroton_Wbxml_Decoder($stream); return $decoder->decode(); } /** * Initialize DAV storage */ protected function getDavStorage() { $sync = \kolab_sync::get_instance(); $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(self::$username) . ':' . rawurlencode(self::$password) . '@', $url); // Make sure user is authenticated $this->getImapStorage(); if (!empty($sync->user)) { // required e.g. for DAV client cache use \rcube::get_instance()->user = $sync->user; } return new \kolab_storage_dav($url); } /** * Initialize IMAP storage */ protected function getImapStorage() { $sync = \kolab_sync::get_instance(); if (!self::$authenticated) { if ($sync->authenticate(self::$username, self::$password)) { self::$authenticated = true; $sync->password = self::$password; } } return $sync->get_storage(); } /** * Check the configured activesync_storage driver */ protected function isStorageDriver($name) { return $name === \kolab_sync::get_instance()->config->get('activesync_storage', 'kolab'); } /** * Make a HTTP request to the ActiveSync server */ protected function request($body, $cmd, $type = 'POST') { $username = self::$username; $deviceId = self::$deviceId; $deviceType = self::$deviceType ?: 'WindowsOutlook15'; $body = $this->toWbxml($body); return self::$client->request( $type, "?Cmd={$cmd}&User={$username}&DeviceId={$deviceId}&DeviceType={$deviceType}", [ 'headers' => [ 'Content-Type' => 'application/vnd.ms-sync.wbxml', 'MS-ASProtocolVersion' => '14.0', ], 'body' => $body, ] ); } /** * Register the device for tests, some commands do not work until device/folders are registered */ protected function registerDevice() { // Execute initial FolderSync, it is required before executing some commands $request = << 0 EOF; $response = $this->request($request, 'FolderSync'); $this->assertEquals(200, $response->getStatusCode()); $dom = $this->fromWbxml($response->getBody()); $xpath = $this->xpath($dom); foreach ($xpath->query("//ns:FolderSync/ns:Changes/ns:Add") as $idx => $folder) { $serverId = $folder->getElementsByTagName('ServerId')->item(0)->nodeValue; $displayName = $folder->getElementsByTagName('DisplayName')->item(0)->nodeValue; $this->folders[$serverId] = $displayName; } } /** * Convert XML into WBXML binary content */ protected function toWbxml($xml) { $outputStream = fopen('php://temp', 'r+'); $encoder = new \Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); $dom = new \DOMDocument(); $dom->loadXML($xml); $encoder->encode($dom); rewind($outputStream); return stream_get_contents($outputStream); } /** * Get XPath from a DOM */ protected function xpath($dom) { $xpath = new \DOMXpath($dom); $xpath->registerNamespace("ns", $dom->documentElement->namespaceURI); $xpath->registerNamespace("AirSync", "uri:AirSync"); $xpath->registerNamespace("AirSyncBase", "uri:AirSyncBase"); $xpath->registerNamespace("Calendar", "uri:Calendar"); $xpath->registerNamespace("Contacts", "uri:Contacts"); $xpath->registerNamespace("Email", "uri:Email"); $xpath->registerNamespace("Email2", "uri:Email2"); $xpath->registerNamespace("Settings", "uri:Settings"); $xpath->registerNamespace("Tasks", "uri:Tasks"); return $xpath; } /** * adapter for phpunit < 9 */ public static function assertMatchesRegularExpression(string $arg1, string $arg2, string $message = ''): void { if (method_exists("PHPUnit\Framework\TestCase", "assertMatchesRegularExpression")) { parent::assertMatchesRegularExpression($arg1, $arg2, $message); } else { parent::assertRegExp($arg1, $arg2); } } }