diff --git a/docs/SQL/mysql.initial.sql b/docs/SQL/mysql.initial.sql --- a/docs/SQL/mysql.initial.sql +++ b/docs/SQL/mysql.initial.sql @@ -107,12 +107,22 @@ 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 */; +CREATE TABLE IF NOT EXISTS `syncroton_subscriptions` ( + `device_id` varchar(40) NOT NULL, + `type` varchar(16) NOT NULL, + `data` longblob NOT NULL, + PRIMARY KEY (`device_id`, `type`), + KEY `syncroton_subscriptions::device_id` (`device_id`), + CONSTRAINT `syncroton_subscriptions::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`) + `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', '2024040800'); diff --git a/docs/SQL/mysql/2024040800.sql b/docs/SQL/mysql/2024040800.sql new file mode 100644 --- /dev/null +++ b/docs/SQL/mysql/2024040800.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `syncroton_subscriptions` ( + `device_id` varchar(40) NOT NULL, + `type` varchar(16) NOT NULL, + `data` longblob NOT NULL, + PRIMARY KEY (`device_id`, `type`), + KEY `syncroton_subscriptions::device_id` (`device_id`), + CONSTRAINT `syncroton_subscriptions::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 */; diff --git a/lib/kolab_sync_backend_device.php b/lib/kolab_sync_backend_device.php --- a/lib/kolab_sync_backend_device.php +++ b/lib/kolab_sync_backend_device.php @@ -31,23 +31,6 @@ protected $table_name = 'syncroton_device'; protected $interface_name = 'Syncroton_Model_IDevice'; - /** - * Kolab Sync storage backend - * - * @var kolab_sync_storage - */ - protected $backend; - - - /** - * Constructor - */ - public function __construct() - { - parent::__construct(); - $this->backend = kolab_sync::storage(); - } - /** * Create (register) a new device * @@ -59,33 +42,23 @@ { $device = parent::create($device); - // Create device entry in kolab backend - $created = $this->backend->device_create([ - 'ID' => $device->id, - 'TYPE' => $device->devicetype, - 'ALIAS' => $device->friendlyname, - ], $device->deviceid); - - if (!$created) { - throw new Syncroton_Exception_NotFound('Device creation failed'); + $sync = kolab_sync::get_instance(); + + // Some devices create dummy devices with name "validate" (#1109) + // This device entry is used in two initial requests, but later + // the device registers a real name. We can remove this dummy entry + // on new device creation + if ($device->deviceid != 'validate') { + $this->db->query( + 'DELETE FROM `' . $this->table_name . '` WHERE `deviceid` = ? AND `owner_id` = ?', + ['validate', $sync->user->ID] + ); } - return $device; - } - - /** - * Delete a device - * - * @param string|Syncroton_Model_IDevice $device Device object - * - * @return bool True on success, False on failure - */ - public function delete($device) - { - // Update IMAP annotation - $this->backend->device_delete($device->deviceid); + // Auto-subscribe a default set of folders + $sync->storage()->device_init($device->deviceid); - return parent::delete($device); + return $device; } /** @@ -109,18 +82,7 @@ throw new Syncroton_Exception_NotFound('Device not found'); } - $device = $this->get_object($device); - - // Make sure device exists (could be deleted by the user) - $dev = $this->backend->device_get($deviceid); - if (empty($dev)) { - // Remove the device (and related cached data) from database - $this->delete($device); - - throw new Syncroton_Exception_NotFound('Device not found'); - } - - return $device; + return $this->get_object($device); } /** diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php --- a/lib/kolab_sync_data.php +++ b/lib/kolab_sync_data.php @@ -334,7 +334,13 @@ */ public function updateFolder(Syncroton_Model_IFolder $folder) { - $result = $this->backend->folder_rename($folder->serverId, $this->device->deviceid, $folder->displayName, $folder->parentId); + $result = $this->backend->folder_rename( + $folder->serverId, + $this->device->deviceid, + $this->modelName, + $folder->displayName, + $folder->parentId + ); if ($result) { return $folder; @@ -353,7 +359,7 @@ } // @TODO: throw exception - return $this->backend->folder_delete($folder, $this->device->deviceid); + return $this->backend->folder_delete($folder, $this->device->deviceid, $this->modelName); } /** @@ -371,7 +377,7 @@ // TODO: Respond with MailboxQuotaExceeded status. Where exactly? foreach ($this->extractFolders($folderid) as $folderid) { - if (!$this->backend->folder_empty($folderid, $this->device->deviceid, !empty($options['deleteSubFolders']))) { + if (!$this->backend->folder_empty($folderid, $this->device->deviceid, $this->modelName, !empty($options['deleteSubFolders']))) { throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR); } } @@ -587,10 +593,16 @@ return $this->searchEntries($folderId, $filter, self::RESULT_COUNT, $syncState->extraData); } - + /** + * Get additional metadata for a specified folder + * + * @param Syncroton_Model_IFolder $folder Folder object + * + * @return string|null JSON-encoded string + */ public function getExtraData(Syncroton_Model_IFolder $folder) { - return $this->backend->getExtraData($folder->serverId, $this->device->deviceid); + return $this->backend->getExtraData($folder->serverId, $this->device->deviceid, $this->modelName); } /** diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php --- a/lib/kolab_sync_data_email.php +++ b/lib/kolab_sync_data_email.php @@ -972,7 +972,7 @@ // @TODO: caching with Options->RebuildResults support foreach ($folders as $folderid) { - $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); + $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid, $this->modelName); if ($foldername === null) { continue; diff --git a/lib/kolab_sync_storage.php b/lib/kolab_sync_storage.php --- a/lib/kolab_sync_storage.php +++ b/lib/kolab_sync_storage.php @@ -51,12 +51,11 @@ public $syncTimeStamp; protected $storage; - protected $folder_meta; protected $folder_uids; protected $folders = []; - protected $root_meta; protected $relations = []; protected $relationSupport = true; + protected $subscriptions; protected $tag_rts = []; private $modseq = []; @@ -116,6 +115,8 @@ // Disable paging $this->storage->set_pagesize(999999); + + $this->subscriptions = new kolab_subscriptions(); } /** @@ -126,29 +127,6 @@ $this->folders = []; } - /** - * List known devices - * - * @return array Device list as hash array - */ - public function devices_list() - { - if ($this->root_meta === null) { - // @TODO: consider server annotation instead of INBOX - if ($meta = $this->storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) { - $this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]); - } else { - $this->root_meta = []; - } - } - - if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) { - return $this->root_meta['DEVICE']; - } - - return []; - } - /** * Get list of folders available for sync * @@ -160,33 +138,15 @@ */ public function folders_list($deviceid, $type, $flat_mode = false) { - // get all folders of specified type - $folders = kolab_storage::list_folders('', '*', $type, false, $typedata); - - // get folders activesync config - $folderdata = $this->folder_meta(); - - if (!is_array($folders) || !is_array($folderdata)) { - return false; - } + $typedata = kolab_storage::folders_typedata(); $folders_list = []; // 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; - } - + foreach ($this->subscriptions->list_subscriptions($deviceid, $type) as $folder => $sub) { // force numeric folder name to be a string (T1283) $folder = (string) $folder; - if (!empty($type) && !in_array($folder, $folders)) { - continue; - } - // Activesync folder identifier (serverId) $folder_type = !empty($typedata[$folder]) ? $typedata[$folder] : 'mail'; $folder_id = $this->folder_id($folder, $folder_type); @@ -256,35 +216,6 @@ return $folders; } - /** - * Getter for folder metadata - * - * @return array|bool Hash array with meta data for each folder, False on backend failure - */ - protected function folder_meta() - { - if (!isset($this->folder_meta)) { - // get folders activesync config - $folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY); - - if (!is_array($folderdata)) { - return $this->folder_meta = false; - } - - $this->folder_meta = []; - - foreach ($folderdata as $folder => $meta) { - if (isset($meta[self::ASYNC_KEY])) { - if ($metadata = $this->unserialize_metadata($meta[self::ASYNC_KEY])) { - $this->folder_meta[$folder] = $metadata; - } - } - } - } - - return $this->folder_meta; - } - /** * Creates folder and subscribes to the device * @@ -299,9 +230,10 @@ { $parent = null; $name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP'); + $type = self::type_activesync2kolab($type); if ($parentid) { - $parent = $this->folder_id2name($parentid, $deviceid); + $parent = $this->folder_id2name($parentid, $deviceid, $type); if ($parent === null) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND); @@ -317,12 +249,10 @@ throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS); } - $type = self::type_activesync2kolab($type); $created = kolab_storage::folder_create($name, $type, true); if ($created) { - // Set ActiveSync subscription flag - $this->folder_set($name, $deviceid, 1); + $this->subscriptions->folder_subscribe($deviceid, $name, 1, $type); return $this->folder_id($name, $type); } @@ -341,17 +271,18 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * @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) + public function folder_rename($folderid, $deviceid, $type, $new_name, $parentid) { - $old_name = $this->folder_id2name($folderid, $deviceid); + $old_name = $this->folder_id2name($folderid, $deviceid, $type); if ($parentid) { - $parent = $this->folder_id2name($parentid, $deviceid); + $parent = $this->folder_id2name($parentid, $deviceid, $type); } $name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP'); @@ -366,18 +297,22 @@ return true; } - $this->folder_meta = null; - // TODO: folder type change? - $type = kolab_storage::folder_type($old_name); - // don't use kolab_storage for moving mail folders - if (preg_match('/^mail/', $type)) { - return $this->storage->rename_folder($old_name, $name); + if ($type == self::MODEL_EMAIL) { + $result = $this->storage->rename_folder($old_name, $name); } else { - return kolab_storage::folder_rename($old_name, $name); + $result = kolab_storage::folder_rename($old_name, $name); } + + if ($result) { + // Set ActiveSync subscription flag + // TODO: Use old subscription flag value + $this->subscriptions->folder_subscribe($deviceid, $name, 1, $type); + } + + return $result; } /** @@ -385,18 +320,16 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * * @return bool True on success, False otherwise */ - public function folder_delete($folderid, $deviceid) + public function folder_delete($folderid, $deviceid, $type) { - $name = $this->folder_id2name($folderid, $deviceid); - $type = kolab_storage::folder_type($name); - - unset($this->folder_meta[$name]); + $name = $this->folder_id2name($folderid, $deviceid, $type); // don't use kolab_storage for deleting mail folders - if (preg_match('/^mail/', $type)) { + if ($type == self::MODEL_EMAIL) { return $this->storage->delete_folder($name); } @@ -408,30 +341,27 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * @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) + public function folder_empty($folderid, $deviceid, $type, $recursive = false) { - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); // Remove all entries if (!$this->storage->clear_folder($foldername)) { return false; } - // Remove subfolders + // Empty subfolders if ($recursive) { $delim = $this->storage->get_hierarchy_delimiter(); - $folderdata = $this->folder_meta(); + $folders = $this->subscriptions->list_subscriptions($deviceid, $type); - if (!is_array($folderdata)) { - return false; - } - - foreach ($folderdata as $subfolder => $meta) { - if (!empty($meta['FOLDER'][$deviceid]['S']) && strpos((string) $subfolder, $foldername . $delim)) { + foreach (array_keys($folders) as $subfolder) { + if (strpos((string) $subfolder, $foldername . $delim)) { if (!$this->storage->clear_folder((string) $subfolder)) { return false; } @@ -442,233 +372,6 @@ return true; } - /** - * Sets ActiveSync subscription flag on a folder - * - * @param string $name Folder name (UTF7-IMAP) - * @param string $deviceid Device identifier - * @param int $flag Flag value (0|1|2) - * - * @return bool True on success, False on failure - */ - protected function folder_set($name, $deviceid, $flag) - { - if (empty($deviceid)) { - return false; - } - - // get folders activesync config - $metadata = $this->folder_meta(); - - if (!is_array($metadata)) { - return false; - } - - $metadata = $metadata[$name] ?? []; - - if ($flag) { - if (empty($metadata)) { - $metadata = []; - } - - if (empty($metadata['FOLDER'])) { - $metadata['FOLDER'] = []; - } - - if (empty($metadata['FOLDER'][$deviceid])) { - $metadata['FOLDER'][$deviceid] = []; - } - - // Z-Push uses: - // 1 - synchronize, no alarms - // 2 - synchronize with alarms - $metadata['FOLDER'][$deviceid]['S'] = $flag; - } else { - unset($metadata['FOLDER'][$deviceid]['S']); - - if (empty($metadata['FOLDER'][$deviceid])) { - unset($metadata['FOLDER'][$deviceid]); - } - - if (empty($metadata['FOLDER'])) { - unset($metadata['FOLDER']); - } - - if (empty($metadata)) { - $metadata = null; - } - } - - // Return if nothing's been changed - if (!self::data_array_diff($this->folder_meta[$name] ?? null, $metadata)) { - return true; - } - - $this->folder_meta[$name] = $metadata; - - return $this->storage->set_metadata($name, [self::ASYNC_KEY => $this->serialize_metadata($metadata)]); - } - - /** - * Returns device metadata - * - * @param string $id Device ID - * - * @return array|null Device metadata - */ - public function device_get($id) - { - $devices_list = $this->devices_list(); - return $devices_list[$id] ?? null; - } - - /** - * Registers new device on server - * - * @param array $device Device data - * @param string $id Device ID - * - * @return bool True on success, False on failure - */ - public function device_create($device, $id) - { - // Fill local cache - $this->devices_list(); - - // Some devices create dummy devices with name "validate" (#1109) - // This device entry is used in two initial requests, but later - // the device registers a real name. We can remove this dummy entry - // on new device creation - $this->device_delete('validate'); - - // Old Kolab_ZPush device parameters - // MODE: -1 | 0 | 1 (not set | flatmode | foldermode) - // TYPE: device type string - // ALIAS: user-friendly device name - - // Syncroton (kolab_sync_backend_device) uses - // ID: internal identifier in syncroton database - // TYPE: device type string - // ALIAS: user-friendly device name - - $metadata = $this->root_meta; - $metadata['DEVICE'][$id] = $device; - $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)]; - - $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); - - if ($result) { - // Update local cache - $this->root_meta['DEVICE'][$id] = $device; - - // subscribe default set of folders - $this->device_init_subscriptions($id); - } - - return $result; - } - - /** - * Device update. - * - * @param array $device Device data - * @param string $id Device ID - * - * @return bool True on success, False on failure - */ - public function device_update($device, $id) - { - $devices_list = $this->devices_list(); - $old_device = $devices_list[$id]; - - if (!$old_device) { - return false; - } - - // Do nothing if nothing is changed - if (!self::data_array_diff($old_device, $device)) { - return true; - } - - $device = array_merge($old_device, $device); - - $metadata = $this->root_meta; - $metadata['DEVICE'][$id] = $device; - $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)]; - - $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); - - if ($result) { - // Update local cache - $this->root_meta['DEVICE'][$id] = $device; - } - - return $result; - } - - /** - * Device delete. - * - * @param string $id Device ID - * - * @return bool True on success, False on failure - */ - public function device_delete($id) - { - $device = $this->device_get($id); - - if (!$device) { - return false; - } - - unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]); - - if (empty($this->root_meta['DEVICE'])) { - unset($this->root_meta['DEVICE']); - } - if (empty($this->root_meta['FOLDER'])) { - unset($this->root_meta['FOLDER']); - } - - $metadata = $this->serialize_metadata($this->root_meta); - $metadata = [self::ASYNC_KEY => $metadata]; - - // update meta data - $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata); - - if ($result) { - // remove device annotation for every folder - foreach ($this->folder_meta() as $folder => $meta) { - // skip root folder (already handled above) - if ($folder == self::ROOT_MAILBOX) { - continue; - } - - if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) { - unset($meta['FOLDER'][$id]); - - if (empty($meta['FOLDER'])) { - unset($this->folder_meta[$folder]['FOLDER']); - unset($meta['FOLDER']); - } - if (empty($meta)) { - unset($this->folder_meta[$folder]); - $meta = null; - } - - $metadata = [self::ASYNC_KEY => $this->serialize_metadata($meta)]; - $res = $this->storage->set_metadata($folder, $metadata); - - if ($res && $meta) { - $this->folder_meta[$folder] = $meta; - } - } - } - } - - return $result; - } - /** * Creates an item in a folder. * @@ -683,7 +386,7 @@ public function createItem($folderid, $deviceid, $type, $data, $params = []) { if ($type == self::MODEL_EMAIL) { - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); $uid = $this->storage->save_message($foldername, $data, '', false, $params['flags'] ?? []); @@ -733,7 +436,7 @@ public function deleteItem($folderid, $deviceid, $type, $uid, $moveToTrash = false) { if ($type == self::MODEL_EMAIL) { - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); $trash = kolab_sync::get_instance()->config->get('trash_mbox'); // move message to the Trash folder @@ -786,7 +489,7 @@ public function updateItem($folderid, $deviceid, $type, $uid, $data, $params = []) { if ($type == self::MODEL_EMAIL) { - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); // Note: We do not support a message body update, as it's not needed @@ -905,7 +608,7 @@ return $this->folders[$unique_key]; } - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); return $this->folders[$unique_key] = kolab_storage::get_folder($foldername, $type); } @@ -921,17 +624,12 @@ */ public function getFolderConfig($folderid, $deviceid, $type) { - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); - $metadata = $this->folder_meta(); - $config = []; - - if (!empty($metadata[$foldername]['FOLDER'][$deviceid])) { - $config = $metadata[$foldername]['FOLDER'][$deviceid]; - } + $subs = $this->subscriptions->folder_subscriptions($foldername, $type); return [ - 'ALARMS' => ($config['S'] ?? 0) == 2, + 'ALARMS' => ($subs[$deviceid] ?? 0) == 2, ]; } @@ -948,7 +646,7 @@ public function getItem($folderid, $deviceid, $type, $uid) { if ($type == self::MODEL_EMAIL) { - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); $message = new rcube_message($uid, $foldername); if (!empty($message->headers)) { @@ -1025,8 +723,8 @@ public function moveItem($srcFolderId, $deviceid, $type, $uid, $dstFolderId) { if ($type === self::MODEL_EMAIL) { - $src_name = $this->folder_id2name($srcFolderId, $deviceid); - $dst_name = $this->folder_id2name($dstFolderId, $deviceid); + $src_name = $this->folder_id2name($srcFolderId, $deviceid, $type); + $dst_name = $this->folder_id2name($dstFolderId, $deviceid, $type); if ($dst_name === null) { throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION); @@ -1170,7 +868,7 @@ $result = $result_type == kolab_sync_data::RESULT_COUNT ? 0 : []; - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); if ($foldername === null) { return $result; @@ -1271,10 +969,11 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * * @return string|null Extra data (JSON-encoded) */ - public function getExtraData($folderid, $deviceid) + public function getExtraData($folderid, $deviceid, $type) { //We explicitly return a cached value that was used during the search. //Otherwise we'd risk storing a higher modseq value and missing an update. @@ -1283,7 +982,7 @@ } //If we didn't fetch modseq in the first place we have to fetch it now. - $foldername = $this->folder_id2name($folderid, $deviceid); + $foldername = $this->folder_id2name($folderid, $deviceid, $type); if ($foldername !== null) { $folder_data = $this->storage->folder_data($foldername); if (!empty($folder_data['HIGHESTMODSEQ'])) { @@ -1598,12 +1297,19 @@ } /** - * Subscribe default set of folders on device registration + * Subscribes to a default set of folder on a new device registration + * + * @param string $deviceid Device ID */ - protected function device_init_subscriptions($deviceid) + public function device_init($deviceid) { - // INBOX always exists - $this->folder_set('INBOX', $deviceid, 1); + $subscribed = [ + 'mail' => ['INBOX' => 1], // INBOX always exists + 'event' => [], + 'contact' => [], + 'task' => [], + 'note' => [], + ]; $supported_types = [ 'mail.drafts', @@ -1640,99 +1346,76 @@ // only personal folders if ($this->storage->folder_namespace($folder) == 'personal') { $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; - $this->folder_set($folder, $deviceid, $flag); + [$type, ] = explode('.', $type); + $subscribed[$type][$folder] = $flag; $folders[] = $folder; } } } - // we're in default mode, exit - if (!$mode) { - return; - } - - // below we support additionally all mail folders - $supported_types[] = 'mail'; - $supported_types[] = 'mail.junkemail'; + if ($mode) { + // below we support additionally all mail folders + $supported_types[] = 'mail'; + $supported_types[] = 'mail.junkemail'; - // get configured special folders - $special_folders = []; - $map = [ - 'drafts' => 'mail.drafts', - 'junk' => 'mail.junkemail', - 'sent' => 'mail.sentitems', - 'trash' => 'mail.wastebasket', - ]; + // get configured special folders + $special_folders = []; + $map = [ + 'drafts' => 'mail.drafts', + 'junk' => 'mail.junkemail', + 'sent' => 'mail.sentitems', + 'trash' => 'mail.wastebasket', + ]; - foreach ($map as $folder => $type) { - if ($folder = $config->get($folder . '_mbox')) { - $special_folders[$folder] = $type; + foreach ($map as $folder => $type) { + if ($folder = $config->get($folder . '_mbox')) { + $special_folders[$folder] = $type; + } } - } - // get folders list(s) - if (($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(); + // get folders list(s) + if (($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(); } - } else { - $all_folders = $this->storage->list_folders_subscribed(); - } - foreach ($all_folders as $folder) { - // folder already subscribed - if (in_array($folder, $folders)) { - continue; - } + foreach ($all_folders as $folder) { + // folder already subscribed + if (in_array($folder, $folders)) { + continue; + } - $type = ($foldertypes[$folder] ?? null) ?: 'mail'; - if ($type == 'mail' && isset($special_folders[$folder])) { - $type = $special_folders[$folder]; - } + $type = ($foldertypes[$folder] ?? null) ?: 'mail'; + if ($type == 'mail' && isset($special_folders[$folder])) { + $type = $special_folders[$folder]; + } - if (!in_array($type, $supported_types)) { - continue; - } + if (!in_array($type, $supported_types)) { + continue; + } - $ns = strtoupper($this->storage->folder_namespace($folder)); + $ns = strtoupper($this->storage->folder_namespace($folder)); - // subscribe the folder according to configured mode - // and folder namespace/subscription status - if (($mode & constant("self::INIT_ALL_{$ns}")) - || (($mode & constant("self::INIT_SUB_{$ns}")) - && (!isset($subscribed_folders) || in_array($folder, $subscribed_folders))) - ) { - $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; - $this->folder_set($folder, $deviceid, $flag); + // subscribe the folder according to configured mode + // and folder namespace/subscription status + if (($mode & constant("self::INIT_ALL_{$ns}")) + || (($mode & constant("self::INIT_SUB_{$ns}")) + && (!isset($subscribed_folders) || in_array($folder, $subscribed_folders))) + ) { + $flag = preg_match('/^(event|task)/', $type) ? 2 : 1; + [$type, ] = explode('.', $type); + $subscribed[$type][$folder] = $flag; + } } } - } - - /** - * Helper method to decode saved IMAP metadata - */ - protected function unserialize_metadata($str) - { - if (!empty($str)) { - $data = json_decode($str, true); - return $data; - } - - return null; - } - /** - * Helper method to encode IMAP metadata for saving - */ - protected function serialize_metadata($data) - { - if (!empty($data) && is_array($data)) { - $data = json_encode($data); - return $data; + foreach ($subscribed as $type => $list) { + $this->subscriptions->set_subscriptions($deviceid, $type, $list); } - - return null; } /** @@ -1817,19 +1500,6 @@ 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. - - // 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). @@ -1854,57 +1524,32 @@ /** * Returns IMAP folder name * - * @param string $id Folder identifier - * @param string $deviceid Device dentifier + * @param string $id Folder identifier + * @param string $deviceid Device dentifier + * @param string $type Folder type * * @return string|null Folder name (UTF7-IMAP) */ - public function folder_id2name($id, $deviceid) + public function folder_id2name($id, $deviceid, $type) { + // TODO: This method should become protected + // check in cache first if (!empty($this->folder_uids)) { if (($name = array_search($id, $this->folder_uids)) !== false) { return $name; } } - /* - @TODO: see folder_id() - - // get folders unique identifier - $folderdata = $this->storage->get_metadata('*', self::UID_KEY); - - foreach ((array)$folderdata as $folder => $data) { - if (!empty($data[self::UID_KEY])) { - $uid = $data[self::UID_KEY]; - $this->folder_uids[$folder] = $uid; - if ($uid == $id) { - $name = $folder; - } - } - } - */ - // get all folders of specified type - $folderdata = $this->folder_meta(); - - if (!is_array($folderdata) || empty($id)) { - return null; - } $name = 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)) { - $this->folder_uids[$folder] = $uid; - } + if (strpos($type, '.')) { + [$type, ] = explode('.', $type); + } - if ($uid === $id) { + // Get the uids of all folders subscribed for activesync + foreach ($this->subscriptions->list_subscriptions($deviceid, $type) as $folder => $props) { + if ($this->folder_id($folder) === $id) { $name = $folder; } } @@ -1990,34 +1635,4 @@ { return kolab_storage::$last_error; } - - /** - * Compares two arrays - * - * @param array $array1 - * @param array $array2 - * - * @return bool True if arrays differs, False otherwise - */ - protected static function data_array_diff($array1, $array2) - { - if (!is_array($array1) || !is_array($array2)) { - return $array1 != $array2; - } - - if (count($array1) != count($array2)) { - return true; - } - - foreach ($array1 as $key => $val) { - if (!array_key_exists($key, $array2)) { - return true; - } - if ($val !== $array2[$key]) { - return true; - } - } - - return false; - } } diff --git a/lib/kolab_sync_storage_kolab4.php b/lib/kolab_sync_storage_kolab4.php --- a/lib/kolab_sync_storage_kolab4.php +++ b/lib/kolab_sync_storage_kolab4.php @@ -77,6 +77,9 @@ // Disable paging $this->storage->set_pagesize(999999); + + // Folders subscriptions engine + $this->subscriptions = new kolab_subscriptions($url); } /** @@ -94,12 +97,6 @@ // 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, @@ -108,13 +105,7 @@ ]; // 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; - } - + foreach ($this->subscriptions->list_subscriptions($deviceid, self::MODEL_EMAIL) as $folder => $meta) { // Force numeric folder name to be a string (T1283) $folder = (string) $folder; @@ -138,10 +129,10 @@ } } - // TODO: For now all DAV folders are subscribed - if (empty($list)) { - foreach ($this->davStorage->get_folders($type) as $folder) { + foreach ($this->subscriptions->list_subscriptions($deviceid, $type) as $folder) { + /** @var kolab_storage_dav_folder $folder */ + $folder = $folder[2]; $folder_data = $this->folder_data($folder, $type); $list[$folder_data['serverId']] = $folder_data; @@ -179,7 +170,7 @@ $name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP'); if ($parentid) { - $parent = $this->folder_id2name($parentid, $deviceid); + $parent = $this->folder_id2name($parentid, $deviceid, self::MODEL_EMAIL); if ($parent === null) { throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND); @@ -201,7 +192,7 @@ if ($created) { // Set ActiveSync subscription flag - $this->folder_set($name, $deviceid, 1); + $this->subscriptions->folder_subscribe($deviceid, $name, 1, self::MODEL_EMAIL); return $this->folder_id($name, 'mail'); } @@ -229,6 +220,10 @@ $props = ['name' => $name, 'type' => $type]; if ($id = $this->davStorage->folder_update($props)) { + // Set ActiveSync subscription flag + $this->subscriptions->folder_subscribe($deviceid, $this->davStorage->new_location, 1, $type); + $this->folders = []; + return "DAV:{$type}:{$id}"; } @@ -243,12 +238,13 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * @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) + public function folder_rename($folderid, $deviceid, $type, $new_name, $parentid) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { @@ -265,10 +261,10 @@ } // Mail folder - $old_name = $this->folder_id2name($folderid, $deviceid); + $old_name = $this->folder_id2name($folderid, $deviceid, $type); if ($parentid) { - $parent = $this->folder_id2name($parentid, $deviceid); + $parent = $this->folder_id2name($parentid, $deviceid, $type); } $name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP'); @@ -282,9 +278,14 @@ return true; } - $this->folder_meta = null; + $result = $this->storage->rename_folder($old_name, $name); + + if ($result) { + // Set ActiveSync subscription flag + $this->subscriptions->folder_subscribe($deviceid, $name, 1, self::MODEL_EMAIL); + } - return $this->storage->rename_folder($old_name, $name); + return $result; } /** @@ -292,10 +293,11 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * * @return bool True on success, False otherwise */ - public function folder_delete($folderid, $deviceid) + public function folder_delete($folderid, $deviceid, $type) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { @@ -305,9 +307,7 @@ } // Mail folder - $name = $this->folder_id2name($folderid, $deviceid); - - unset($this->folder_meta[$name]); + $name = $this->folder_id2name($folderid, $deviceid, $type); return $this->storage->delete_folder($name); } @@ -317,11 +317,12 @@ * * @param string $folderid Folder identifier * @param string $deviceid Device identifier + * @param string $type Activesync model name (folder type) * @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) + public function folder_empty($folderid, $deviceid, $type, $recursive = false) { // DAV folder if (strpos($folderid, 'DAV:') === 0) { @@ -337,7 +338,7 @@ } // Mail folder - return parent::folder_empty($folderid, $deviceid, $recursive); + return parent::folder_empty($folderid, $deviceid, $type, $recursive); } /** @@ -388,20 +389,7 @@ 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). @@ -418,50 +406,19 @@ /** * Returns IMAP folder name * - * @param string $id Folder identifier - * @param string $deviceid Device dentifier + * @param string $id Folder identifier + * @param string $deviceid Device dentifier + * @param string $type Folder type * * @return null|string Folder name (UTF7-IMAP) */ - public function folder_id2name($id, $deviceid) + public function folder_id2name($id, $deviceid, $type) { - // 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; + return parent::folder_id2name($id, $deviceid, $type); } /** @@ -471,7 +428,7 @@ * @param string $deviceid Device identifier * @param string $type Activesync model name (folder type) * - * @return ?kolab_storage_folder + * @return ?kolab_storage_dav_folder */ public function getFolder($folderid, $deviceid, $type) { @@ -501,10 +458,15 @@ */ public function getFolderConfig($folderid, $deviceid, $type) { - // TODO: Get "alarms" from the DAV folder props, or implement - // a storage for folder properties + $alarms = 0; + + if ($folder = $this->getFolder($folderid, $deviceid, $type)) { + $subs = $this->subscriptions->folder_subscriptions($folder->href, $type); + $alarms = $subs[$deviceid] ?? 0; + } + return [ - 'ALARMS' => true, + 'ALARMS' => $alarms == 2, ]; } @@ -518,9 +480,11 @@ } /** - * Subscribe default set of folders on device registration + * Subscribes to a default set of folder on a new device registration + * + * @param string $deviceid Device ID */ - protected function device_init_subscriptions($deviceid) + public function device_init($deviceid) { $config = rcube::get_instance()->config; $mode = (int) $config->get('activesync_init_subscriptions'); @@ -544,6 +508,13 @@ $all_folders = $this->storage->list_folders_subscribed(); } + $subscribe = [ + 'mail' => ['INBOX' => 1], + 'contact' => [], + 'event' => [], + 'task' => [], + ]; + foreach ($all_folders as $folder) { $ns = strtoupper($this->storage->folder_namespace($folder)); @@ -553,19 +524,28 @@ || ($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); + $subscribe['mail'][$folder] = 1; } } - // TODO: Subscribe personal DAV folders, for now we assume all are subscribed - // TODO: Subscribe shared DAV folders + foreach ($subscribe as $type => $list) { + if ($type != 'mail') { + foreach ($this->subscriptions->list_folders($type) as $folder) { + // TODO: Subscribe personal DAV folders, for now we assume all are subscribed + $list[$folder[0]] = ($type == 'event' || $type == 'task') ? 2 : 1; + } + } + + $this->subscriptions->set_subscriptions($deviceid, $type, $list); + } } - public function getExtraData($folderid, $deviceid) + public function getExtraData($folderid, $deviceid, $type) { if (strpos($folderid, 'DAV:') === 0) { return null; } - return parent::getExtraData($folderid, $deviceid); + + return parent::getExtraData($folderid, $deviceid, $type); } } diff --git a/tests/SyncTestCase.php b/tests/SyncTestCase.php --- a/tests/SyncTestCase.php +++ b/tests/SyncTestCase.php @@ -46,11 +46,8 @@ 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'); self::$client = new \GuzzleHttp\Client([ 'http_errors' => false, @@ -75,18 +72,16 @@ { 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_data'); + $db->query('DELETE FROM syncroton_data_folder'); } }