Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F16569759
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
73 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/kolab_sync_storage.php b/lib/kolab_sync_storage.php
index a1c54f3..35ad54a 100644
--- a/lib/kolab_sync_storage.php
+++ b/lib/kolab_sync_storage.php
@@ -1,2066 +1,2070 @@
<?php
/*
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Storage handling class with basic Kolab support (everything stored in IMAP)
*/
class kolab_sync_storage
{
public const INIT_SUB_PERSONAL = 1; // all subscribed folders in personal namespace
public const INIT_ALL_PERSONAL = 2; // all folders in personal namespace
public const INIT_SUB_OTHER = 4; // all subscribed folders in other users namespace
public const INIT_ALL_OTHER = 8; // all folders in other users namespace
public const INIT_SUB_SHARED = 16; // all subscribed folders in shared namespace
public const INIT_ALL_SHARED = 32; // all folders in shared namespace
public const MODEL_CALENDAR = 'event';
public const MODEL_CONTACTS = 'contact';
public const MODEL_EMAIL = 'mail';
public const MODEL_NOTES = 'note';
public const MODEL_TASKS = 'task';
public const ROOT_MAILBOX = 'INBOX';
public const ASYNC_KEY = '/private/vendor/kolab/activesync';
public const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
public const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
public const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
public $syncTimeStamp;
protected $storage;
protected $folder_meta;
protected $folder_uids;
protected $folders = [];
protected $root_meta;
protected $relations = [];
public $relationSupport = true;
protected $tag_rts = [];
private $modseq = [];
protected static $instance;
protected static $types = [
1 => '',
2 => 'mail.inbox',
3 => 'mail.drafts',
4 => 'mail.wastebasket',
5 => 'mail.sentitems',
6 => 'mail.outbox',
7 => 'task.default',
8 => 'event.default',
9 => 'contact.default',
10 => 'note.default',
11 => 'journal.default',
12 => 'mail',
13 => 'event',
14 => 'contact',
15 => 'task',
16 => 'journal',
17 => 'note',
];
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync_storage The one and only instance
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_sync_storage();
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Class initialization
*/
public function startup()
{
$this->storage = kolab_sync::get_instance()->get_storage();
// set additional header used by libkolab
$this->storage->set_options([
// @TODO: there can be Roundcube plugins defining additional headers,
// we maybe would need to add them here
'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION',
'skip_deleted' => true,
'threading' => false,
]);
// Disable paging
$this->storage->set_pagesize(999999);
}
/**
* Clear internal cache state
*/
public function reset()
{
$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
*
* @param string $deviceid Device identifier
* @param string $type Folder 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)
{
// 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;
}
$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;
}
// 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);
$folders_list[$folder_id] = $this->folder_data($folder, $folder_type);
}
if ($flat_mode) {
$folders_list = $this->folders_list_flat($folders_list, $type, $typedata);
}
return $folders_list;
}
/**
* Converts list of folders to a "flat" list
*/
protected function folders_list_flat($folders, $type, $typedata)
{
$delim = $this->storage->get_hierarchy_delimiter();
foreach ($folders as $idx => $folder) {
if ($folder['parentId']) {
// for non-mail folders we make the list completely flat
if ($type != self::MODEL_EMAIL) {
$display_name = kolab_storage::object_name($folder['imap_name']);
$display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
$folders[$idx]['parentId'] = 0;
$folders[$idx]['displayName'] = $display_name;
}
// for mail folders we modify only folders with non-existing parents
elseif (!isset($folders[$folder['parentId']])) {
$items = explode($delim, $folder['imap_name']);
$parent = 0;
// find existing parent
while (count($items) > 0) {
array_pop($items);
$parent_name = implode($delim, $items);
$parent_type = !empty($typedata[$parent_name]) ? $typedata[$parent_name] : 'mail';
$parent_id = $this->folder_id($parent_name, $parent_type);
if (isset($folders[$parent_id])) {
$parent = $parent_id;
break;
}
}
if (!$parent) {
$display_name = kolab_storage::object_name($folder['imap_name']);
$display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
} else {
$parent_name = isset($parent_id) ? $folders[$parent_id]['imap_name'] : '';
$display_name = substr($folder['imap_name'], strlen($parent_name) + 1);
$display_name = rcube_charset::convert($display_name, 'UTF7-IMAP');
$display_name = str_replace($delim, ' » ', $display_name);
}
$folders[$idx]['parentId'] = $parent;
$folders[$idx]['displayName'] = $display_name;
}
}
}
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
*
* @param string $name Folder name (UTF8)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
* @param ?string $parentid Parent folder id identifier
*
* @return string|false New folder identifier on success, False on failure
*/
public function folder_create($name, $type, $deviceid, $parentid = null)
{
$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);
}
$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);
return $this->folder_id($name, $type);
}
// 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_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER);
}
return false;
}
/**
* 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)
{
$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;
}
// Rename/move IMAP folder
if ($name === $old_name) {
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);
} else {
return kolab_storage::folder_rename($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)
{
$name = $this->folder_id2name($folderid, $deviceid);
$type = kolab_storage::folder_type($name);
unset($this->folder_meta[$name]);
// don't use kolab_storage for deleting mail folders
if (preg_match('/^mail/', $type)) {
return $this->storage->delete_folder($name);
}
return kolab_storage::folder_delete($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)
{
$foldername = $this->folder_id2name($folderid, $deviceid);
// Remove all entries
if (!$this->storage->clear_folder($foldername)) {
return false;
}
// Remove subfolders
if ($recursive) {
$delim = $this->storage->get_hierarchy_delimiter();
$folderdata = $this->folder_meta();
if (!is_array($folderdata)) {
return false;
}
foreach ($folderdata as $subfolder => $meta) {
if (!empty($meta['FOLDER'][$deviceid]['S']) && strpos((string) $subfolder, $foldername . $delim)) {
if (!$this->storage->clear_folder((string) $subfolder)) {
return false;
}
}
}
}
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.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string|array $data Object data (string for email, array for other types)
* @param array $params Additional parameters (e.g. mail flags)
*
* @return string|null Item UID on success or null on failure
*/
public function createItem($folderid, $deviceid, $type, $data, $params = [])
{
if ($type == self::MODEL_EMAIL) {
$foldername = $this->folder_id2name($folderid, $deviceid);
$uid = $this->storage->save_message($foldername, $data, '', false, $params['flags'] ?? []);
if (!$uid) {
// $this->logger->error("Error while storing the message " . $this->storage->get_error_str());
}
return $uid;
}
$useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
// convert categories into tags, save them after creating an object
if ($useTags && !empty($data['categories'])) {
$tags = $data['categories'];
unset($data['categories']);
}
$folder = $this->getFolder($folderid, $deviceid, $type);
// Set User-Agent for saved objects
$app = kolab_sync::get_instance();
$app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
if ($folder && $folder->valid && $folder->save($data)) {
if (!empty($tags) && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES)) {
$this->setCategories($data['uid'], $tags);
}
return $data['uid'];
}
return null;
}
/**
* Deletes an item from a folder by UID.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Requested object UID
* @param bool $moveToTrash Move to trash, instead of delete (for mail messages only)
*
* @return bool True on success, False on failure
*/
public function deleteItem($folderid, $deviceid, $type, $uid, $moveToTrash = false)
{
if ($type == self::MODEL_EMAIL) {
$foldername = $this->folder_id2name($folderid, $deviceid);
$trash = kolab_sync::get_instance()->config->get('trash_mbox');
// move message to the Trash folder
if ($moveToTrash && strlen($trash) && $trash != $foldername && $this->storage->folder_exists($trash)) {
return $this->storage->move_message($uid, $trash, $foldername);
}
// delete the message
// According to the ActiveSync spec. "If the DeletesAsMoves element is set to false,
// the deletion is PERMANENT.", therefore we delete the message, and not flag as deleted.
// FIXME: We could consider acting according to the 'flag_for_deletion' setting.
// Don't forget about 'read_when_deleted' setting then.
// $this->storage->set_flag($uid, 'DELETED', $foldername);
// $this->storage->set_flag($uid, 'SEEN', $foldername);
return $this->storage->delete_message($uid, $foldername);
}
$useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
if ($folder->delete($uid)) {
if ($useTags) {
$this->setCategories($uid, []);
}
return true;
}
return false;
}
/**
* Updates an item in a folder.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Object UID
* @param string|array $data Object data (string for email, array for other types)
* @param array $params Additional parameters (e.g. mail flags)
*
* @return string|null Item UID on success or null on failure
*/
public function updateItem($folderid, $deviceid, $type, $uid, $data, $params = [])
{
if ($type == self::MODEL_EMAIL) {
$foldername = $this->folder_id2name($folderid, $deviceid);
// Note: We do not support a message body update, as it's not needed
foreach (($params['flags'] ?? []) as $flag) {
$this->storage->set_flag($uid, $flag, $foldername);
}
// Categories (Tags) change
if (isset($params['categories']) && $this->relationSupport) {
$message = new rcube_message($uid, $foldername);
if (empty($message->headers)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
$this->setCategories($message, $params['categories']);
}
return $uid;
}
$folder = $this->getFolder($folderid, $deviceid, $type);
$useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
// convert categories into tags, save them after updating an object
if ($useTags && array_key_exists('categories', $data)) {
$tags = (array) $data['categories'];
unset($data['categories']);
}
// Set User-Agent for saved objects
$app = kolab_sync::get_instance();
$app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
if ($folder && $folder->valid && $folder->save($data, $type, $uid)) {
if (isset($tags)) {
$this->setCategories($uid, $tags);
}
return $uid;
}
return null;
}
/**
* Returns list of categories assigned to an object
*
* @param object|string $object UID or rcube_message object
* @param array $categories Addition tag names to merge with
*
* @return array List of categories
*/
public function getCategories($object, $categories = [])
{
if (is_object($object)) {
// support only messages with message-id
if (!($msg_id = $object->headers->get('message-id', false))) {
return [];
}
$config = kolab_storage_config::get_instance();
$delta = Syncroton_Registry::getPingTimeout();
$folder = $object->folder;
$uid = $object->uid;
// get tag objects raleted to specified message-id
$tags = $config->get_tags($msg_id);
foreach ($tags as $idx => $tag) {
// resolve members if it wasn't done recently
$force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta;
$members = $config->resolve_members($tag, $force);
if (empty($members[$folder]) || !in_array($uid, $members[$folder])) {
unset($tags[$idx]);
}
if ($force) {
$this->tag_rts[$tag['uid']] = time();
}
}
// make sure current folder is set correctly again
$this->storage->set_folder($folder);
} else {
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($object);
}
$tags = array_filter(array_map(function ($v) { return $v['name']; }, $tags));
// merge result with old categories
if (!empty($categories)) {
$tags = array_unique(array_merge($tags, (array) $categories));
}
return $tags;
}
/**
* 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)
{
$unique_key = "$folderid:$deviceid:$type";
if (array_key_exists($unique_key, $this->folders)) {
return $this->folders[$unique_key];
}
$foldername = $this->folder_id2name($folderid, $deviceid);
return $this->folders[$unique_key] = kolab_storage::get_folder($foldername, $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)
{
$foldername = $this->folder_id2name($folderid, $deviceid);
$metadata = $this->folder_meta();
$config = [];
if (!empty($metadata[$foldername]['FOLDER'][$deviceid])) {
$config = $metadata[$foldername]['FOLDER'][$deviceid];
}
return [
'ALARMS' => ($config['S'] ?? 0) == 2,
];
}
/**
* Gets an item from a folder by UID.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Requested object UID
*
* @return array|rcube_message|null Object properties
*/
public function getItem($folderid, $deviceid, $type, $uid)
{
if ($type == self::MODEL_EMAIL) {
$foldername = $this->folder_id2name($folderid, $deviceid);
$message = new rcube_message($uid, $foldername);
if (!empty($message->headers)) {
if ($this->relationSupport) {
$message->headers->others['categories'] = $this->getCategories($message);
}
return $message;
}
return null;
}
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = $folder->get_object($uid);
if ($result === false) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
if ($useTags) {
$result['categories'] = $this->getCategories($uid, $result['categories'] ?? []);
}
return $result;
}
/**
* Gets items matching UID by prefix.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Requested object UID prefix
*
* @return array|iterable List of objects
*/
public function getItemsByUidPrefix($folderid, $deviceid, $type, $uid)
{
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = $folder->select([['uid', '~*', $uid]]);
if ($result === null) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
return $result;
}
/**
* Move an item from one folder to another.
*
* @param string $srcFolderId Source folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Object UID
* @param string $dstFolderId Destination folder identifier
*
* @return string New object UID
* @throws Syncroton_Exception_Status
*/
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);
if ($dst_name === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if ($src_name === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
if (!$this->storage->move_message($uid, $dst_name, $src_name)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
// Use COPYUID feature (RFC2359) to get the new UID of the copied message
if (empty($this->storage->conn->data['COPYUID'])) {
// Check if the source item actually exists. Cyrus IMAP reports
// OK on a MOVE with an invalid UID, But COPYUID will be empty.
// This way we only incur the cost of the extra check once the move fails.
if (!$this->storage->get_message_headers($uid, $src_name)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
return $this->storage->conn->data['COPYUID'][1];
}
$srcFolder = $this->getFolder($srcFolderId, $deviceid, $type);
$dstFolder = $this->getFolder($dstFolderId, $deviceid, $type);
if (!$srcFolder || !$dstFolder) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if (!$srcFolder->move($uid, $dstFolder)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
return $uid;
}
/**
* Set categories to an object
*
* @param object|string $object UID or rcube_message object
* @param array $categories List of Category names
*/
public function setCategories($object, $categories)
{
if (!is_object($object)) {
$config = kolab_storage_config::get_instance();
$config->save_tags($object, $categories);
return;
}
$config = kolab_storage_config::get_instance();
$delta = Syncroton_Registry::getPingTimeout();
$uri = kolab_storage_config::get_message_uri($object->headers, $object->folder);
// for all tag objects...
foreach ($config->get_tags() as $relation) {
// resolve members if it wasn't done recently
$uid = $relation['uid'];
$force = empty($this->tag_rts[$uid]) || $this->tag_rts[$uid] <= time() - $delta;
if ($force) {
$config->resolve_members($relation, $force);
$this->tag_rts[$relation['uid']] = time();
}
$selected = !empty($categories) && in_array($relation['name'], $categories);
$found = !empty($relation['members']) && in_array($uri, $relation['members']);
$update = false;
// remove member from the relation
if ($found && !$selected) {
$relation['members'] = array_diff($relation['members'], (array) $uri);
$update = true;
}
// add member to the relation
elseif (!$found && $selected) {
$relation['members'][] = $uri;
$update = true;
}
if ($update) {
$config->save($relation, 'relation');
}
$categories = array_diff($categories, (array) $relation['name']);
}
// create new relations
if (!empty($categories)) {
foreach ($categories as $tag) {
$relation = [
'name' => $tag,
'members' => (array) $uri,
'category' => 'tag',
];
$config->save($relation, 'relation');
}
}
// make sure current folder is set correctly again
$this->storage->set_folder($object->folder);
}
/**
* Search for existing objects in a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $device_key Device primary key
* @param string $type Activesync model name (folder type)
* @param array $filter Filter
* @param int $result_type Type of the result (see kolab_sync_data::RESULT_* constants)
* @param bool $force Force IMAP folder cache synchronization
* @param string $extraData Extra data as extracted by the getExtraData during the last sync
*
* @return array|int Search result as count or array of uids
*/
public function searchEntries($folderid, $deviceid, $device_key, $type, $filter, $result_type, $force, $extraData)
{
if ($type != self::MODEL_EMAIL) {
return $this->searchKolabEntries($folderid, $deviceid, $device_key, $type, $filter, $result_type, $force);
}
$filter_str = 'ALL UNDELETED';
$getChangesMode = false;
// convert filter into one IMAP search string
foreach ($filter as $idx => $filter_item) {
if (is_array($filter_item)) {
if ($filter_item[0] == 'changed' && $filter_item[1] == '>') {
$getChangesMode = true;
}
} else {
$filter_str .= ' ' . $filter_item;
}
}
$result = $result_type == kolab_sync_data::RESULT_COUNT ? 0 : [];
$foldername = $this->folder_id2name($folderid, $deviceid);
if ($foldername === null) {
return $result;
}
$this->storage->set_folder($foldername);
// Synchronize folder (if it wasn't synced in this request already)
if ($force) {
$this->storage->folder_sync($foldername);
}
$modified = true;
// We're in "get changes" mode
if ($getChangesMode) {
$folder_data = $this->storage->folder_data($foldername);
// If HIGHESTMODSEQ doesn't exist we can't get changes
if (!empty($folder_data['HIGHESTMODSEQ'])) {
// Store modseq for later in getExtraData
if (!array_key_exists($deviceid, $this->modseq)) {
$this->modseq[$deviceid] = [];
}
$this->modseq[$deviceid][$folderid] = $folder_data['HIGHESTMODSEQ'];
// After the initial sync we have no extraData
if ($extraData) {
$modseq_old = json_decode($extraData)->modseq;
// Skip search if HIGHESTMODSEQ didn't change
if ($folder_data['HIGHESTMODSEQ'] == $modseq_old) {
$modified = false;
} else {
$filter_str .= " MODSEQ " . ($modseq_old + 1);
}
} else {
// If we don't have extra data we can't search for changes.
// Either we are in initial sync, which means there are no changes to find,
// or we are in the migration (no previous extraData), in which case we ignore changes for one sync key
// because we don't have the means to search for the changes. Going forward we'll have the modseq info.
$modified = false;
}
} else {
// We have no way of finding the changes.
// We could fall back to search by date or ignore changes, but both seems suboptimal.
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
}
// We could use messages cache by replacing search() with index()
// in some cases. This however is possible only if user has skip_deleted=true,
// in his Roundcube preferences, otherwise we'd make often cache re-initialization,
// because Roundcube message cache can work only with one skip_deleted
// setting at a time. We'd also need to make sure folder_sync() was called
// before (see above).
//
// if ($filter_str == 'ALL UNDELETED')
// $search = $this->storage->index($foldername, null, null, true, true);
// else
if ($modified) {
$search = $this->storage->search_once($foldername, $filter_str);
if (!($search instanceof rcube_result_index) || $search->is_error()) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
$result = $search->count();
break;
case kolab_sync_data::RESULT_UID:
$result = $search->get();
break;
}
}
// get members of modified relations
if ($this->relationSupport) {
$changed_msgs = $this->getChangesByRelations($folderid, $device_key, $type, $filter);
// handle relation changes
if (!empty($changed_msgs)) {
$members = $this->findRelationMembersInFolder($foldername, $changed_msgs, $filter);
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
$result += count($members);
break;
case kolab_sync_data::RESULT_UID:
$result = array_values(array_unique(array_merge($result, $members)));
break;
}
}
}
return $result;
}
/**
* Return extra data that is stored with the sync key and passed in during the search to find changes.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
*
* @return string|null Extra data (JSON-encoded)
*/
public function getExtraData($folderid, $deviceid)
{
//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.
if (isset($this->modseq[$deviceid][$folderid])) {
return json_encode(['modseq' => intval($this->modseq[$deviceid][$folderid])]);
}
//If we didn't fetch modseq in the first place we have to fetch it now.
$foldername = $this->folder_id2name($folderid, $deviceid);
if ($foldername !== null) {
$folder_data = $this->storage->folder_data($foldername);
if (!empty($folder_data['HIGHESTMODSEQ'])) {
return json_encode(['modseq' => intval($folder_data['HIGHESTMODSEQ'])]);
}
}
return null;
}
/**
* Search for existing objects in a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $device_key Device primary key
* @param string $type Activesync model name (folder type)
* @param array $filter Filter
* @param int $result_type Type of the result (see kolab_sync_data::RESULT_* constants)
* @param bool $force Force IMAP folder cache synchronization
*
* @return array|int Search result as count or array of uids
*/
protected function searchKolabEntries($folderid, $deviceid, $device_key, $type, $filter, $result_type, $force)
{
// there's a PHP Warning from kolab_storage if $filter isn't an array
if (empty($filter)) {
$filter = [];
} elseif ($this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES)) {
$changed_objects = $this->getChangesByRelations($folderid, $device_key, $type, $filter);
}
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$error = false;
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
$count = $folder->count($filter);
if ($count === null) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = (int) $count;
break;
case kolab_sync_data::RESULT_UID:
default:
$uids = $folder->get_uids($filter);
if (!is_array($uids)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = $uids;
break;
}
// handle tag modifications
if (!empty($changed_objects)) {
// build new filter
// search objects mathing current filter,
// relations may contain members of many types, we need to
// search them by UID in all requested folders to get
// only these with requested type (and that really exist
// in specified folders)
$tag_filter = [['uid', '=', $changed_objects]];
foreach ($filter as $f) {
if ($f[0] != 'changed') {
$tag_filter[] = $f;
}
}
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
// Note: this way we're potentally counting the same objects twice
// I'm not sure if this is a problem, we most likely do not
// need a precise result here
$count = $folder->count($tag_filter);
if ($count !== null) {
$result += (int) $count;
}
break;
case kolab_sync_data::RESULT_UID:
default:
$uids = $folder->get_uids($tag_filter);
if (is_array($uids) && !empty($uids)) {
$result = array_unique(array_merge($result, $uids));
}
break;
}
}
return $result;
}
/**
* Find members (messages) in specified folder
*/
protected function findRelationMembersInFolder($foldername, $members, $filter)
{
foreach ($members as $member) {
// IMAP URI members
if ($url = kolab_storage_config::parse_member_url($member)) {
$result[$url['folder']][$url['uid']] = $url['params'];
}
}
// convert filter into one IMAP search string
$filter_str = 'ALL UNDELETED';
foreach ($filter as $filter_item) {
if (is_string($filter_item)) {
$filter_str .= ' ' . $filter_item;
}
}
$found = [];
// first find messages by UID
if (!empty($result[$foldername])) {
$index = $this->storage->search_once($foldername, 'UID '
. rcube_imap_generic::compressMessageSet(array_keys($result[$foldername])));
$found = $index->get();
// remove found messages from the $result
if (!empty($found)) {
$result[$foldername] = array_diff_key($result[$foldername], array_flip($found));
if (empty($result[$foldername])) {
unset($result[$foldername]);
}
// now apply the current filter to the found messages
$index = $this->storage->search_once($foldername, $filter_str . ' UID '
. rcube_imap_generic::compressMessageSet($found));
$found = $index->get();
}
}
// search by message parameters
if (!empty($result)) {
// @TODO: do this search in chunks (for e.g. 25 messages)?
$search = '';
$search_count = 0;
foreach ($result as $data) {
foreach ($data as $p) {
$search_params = [];
$search_count++;
foreach ($p as $key => $val) {
$key = strtoupper($key);
// don't search by subject, we don't want false-positives
if ($key != 'SUBJECT') {
$search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
}
}
$search .= ' (' . implode(' ', $search_params) . ')';
}
}
$search_str = str_repeat(' OR', $search_count - 1) . $search;
// search messages in current folder
$search = $this->storage->search_once($foldername, $search_str);
$uids = $search->get();
if (!empty($uids)) {
// add UIDs into the result
$found = array_unique(array_merge($found, $uids));
}
}
return $found;
}
/**
* Detect changes of relation (tag) objects data and assigned objects
* Returns relation member identifiers
*/
protected function getChangesByRelations($folderid, $device_key, $type, $filter)
{
// get period filter, create new objects filter
foreach ($filter as $f) {
if ($f[0] == 'changed' && $f[1] == '>') {
$since = $f[2];
}
}
// this is not search for changes, do nothing
if (empty($since)) {
return;
}
// get relations state from the last sync
$last_state = (array) $this->relations_state_get($device_key, $folderid, $since);
// get current relations state
$config = kolab_storage_config::get_instance();
$default = true;
$filter = [
['type', '=', 'relation'],
['category', '=', 'tag'],
];
$relations = $config->get_objects($filter, $default, 100);
$result = [];
$changed = false;
// compare states, get members of changed relations
foreach ($relations as $relation) {
$rel_id = $relation['uid'];
if ($relation['changed']) {
$relation['changed']->setTimezone(new DateTimeZone('UTC'));
}
// last state unknown...
if (empty($last_state[$rel_id])) {
// ...get all members
if (!empty($relation['members'])) {
$changed = true;
$result = array_merge($result, $relation['members']);
}
}
// last state known, changed tag name...
elseif ($last_state[$rel_id]['name'] != $relation['name']) {
// ...get all (old and new) members
$members_old = explode("\n", $last_state[$rel_id]['members']);
$changed = true;
$members = array_unique(array_merge($relation['members'], $members_old));
$result = array_merge($result, $members);
}
// last state known, any other change change...
elseif ($last_state[$rel_id]['changed'] < $relation['changed']->format('U')) {
// ...find new and removed members
$members_old = explode("\n", $last_state[$rel_id]['members']);
$new = array_diff($relation['members'], $members_old);
$removed = array_diff($members_old, $relation['members']);
if (!empty($new) || !empty($removed)) {
$changed = true;
$result = array_merge($result, $new, $removed);
}
}
unset($last_state[$rel_id]);
}
// get members of deleted relations
if (!empty($last_state)) {
$changed = true;
foreach ($last_state as $relation) {
$members = explode("\n", $relation['members']);
$result = array_merge($result, $members);
}
}
// save current state
if ($changed) {
$data = [];
foreach ($relations as $relation) {
$data[$relation['uid']] = [
'name' => $relation['name'],
'changed' => $relation['changed']->format('U'),
'members' => implode("\n", (array)$relation['members']),
];
}
// If the new and the old timestamp are the same our cache breaks.
// We must preserve the previous changes, because if this function is rerun we must detect the same changes again.
$sinceFormatted = $since->format('Y-m-d H:i:s');
if ($this->syncTimeStamp->format('Y-m-d H:i:s') == $sinceFormatted) {
// Preserve the previous timestamp (relations_state_get just checks the overflow bucket first)
// FIXME: The one caveat is that we will still update the database and thus overwrite the old entry.
// That means if we rerun the same request, the changes will not be detected
// => We should not be dealing with timestamps really.
$this->relations[$folderid][$sinceFormatted . "-1"] = $this->relations[$folderid][$sinceFormatted] ?? null;
$this->relations[$folderid][$sinceFormatted] = null;
}
$this->relations_state_set($device_key, $folderid, $this->syncTimeStamp, $data);
}
// in mail mode return only message URIs
if ($type == self::MODEL_EMAIL) {
// lambda function to skip email members
$filter_func = function ($value) {
return strpos($value, 'imap://') === 0;
};
$result = array_filter(array_unique($result), $filter_func);
}
// otherwise return only object UIDs
else {
// lambda function to skip email members
$filter_func = function ($value) {
return strpos($value, 'urn:uuid:') === 0;
};
// lambda function to parse member URI
$member_func = function ($value) {
if (strpos($value, 'urn:uuid:') === 0) {
$value = substr($value, 9);
}
return $value;
};
$result = array_map($member_func, array_filter(array_unique($result), $filter_func));
}
return $result;
}
/**
* Subscribe default set of folders on device registration
*/
protected function device_init_subscriptions($deviceid)
{
// INBOX always exists
$this->folder_set('INBOX', $deviceid, 1);
$supported_types = [
'mail.drafts',
'mail.wastebasket',
'mail.sentitems',
'mail.outbox',
'event.default',
'contact.default',
'note.default',
'task.default',
'event',
'contact',
'note',
'task',
'event.confidential',
'event.private',
'task.confidential',
'task.private',
];
$rcube = rcube::get_instance();
$config = $rcube->config;
$mode = (int) $config->get('activesync_init_subscriptions');
$folders = [];
// Subscribe to default folders
$foldertypes = kolab_storage::folders_typedata();
if (!empty($foldertypes)) {
$_foldertypes = array_intersect($foldertypes, $supported_types);
// get default folders
foreach ($_foldertypes as $folder => $type) {
// only personal folders
if ($this->storage->folder_namespace($folder) == 'personal') {
$flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
$this->folder_set($folder, $deviceid, $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';
// 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;
}
}
// 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();
}
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];
}
if (!in_array($type, $supported_types)) {
continue;
}
$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);
}
}
}
/**
* 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;
}
return null;
}
/**
* Returns Kolab folder type for specified ActiveSync type ID
*/
protected static function type_activesync2kolab($type)
{
if (!empty(self::$types[$type])) {
return self::$types[$type];
}
return '';
}
/**
* Returns ActiveSync folder type for specified Kolab type
*/
protected static function type_kolab2activesync($type)
{
$type = preg_replace('/\.(confidential|private)$/i', '', $type);
if ($key = array_search($type, self::$types)) {
return $key;
}
return key(self::$types);
}
/**
* Returns folder data in Syncroton format
*/
protected function folder_data($folder, $type)
{
// Folder name parameters
$delim = $this->storage->get_hierarchy_delimiter();
$items = explode($delim, $folder);
$name = array_pop($items);
// Folder UID
$folder_id = $this->folder_id($folder, $type);
// Folder type
if (strcasecmp($folder, 'INBOX') === 0) {
// INBOX is always inbox, prevent from issues related with a change of
// folder type annotation (it can be initially unset).
$as_type = 2;
} else {
$as_type = self::type_kolab2activesync($type);
// fix type, if there's no type annotation it's detected as UNKNOWN we'll use 'mail' (12)
if ($as_type == 1) {
$as_type = 12;
}
}
// Syncroton folder data array
return [
'serverId' => $folder_id,
'parentId' => count($items) ? $this->folder_id(implode($delim, $items), $type) : 0,
'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET),
'type' => $as_type,
// for internal use
'imap_name' => $folder,
];
}
/**
* Builds folder ID based on folder name
*/
protected function folder_id($name, $type = null)
{
// 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 (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.
// 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';
} else {
if ($type === null) {
$type = kolab_storage::folder_type($name);
}
if ($type != null) {
$type = preg_replace('/\.(confidential|private)$/i', '', $type);
}
}
// 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 string|null Folder name (UTF7-IMAP)
*/
public function folder_id2name($id, $deviceid)
{
// 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 ($uid === $id) {
$name = $folder;
}
}
return $name;
}
/**
* Set state of relation objects at specified point in time
*/
public function relations_state_set($device_key, $folderid, $synctime, $relations)
{
$synctime = $synctime->format('Y-m-d H:i:s');
// Protect against inserting the same values twice (this code can be executed twice in the same request)
if (!isset($this->relations[$folderid][$synctime])) {
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$this->relations[$folderid][$synctime] = $relations;
- $data = rcube_charset::clean(json_encode($relations));
+ $data = gzdeflate(json_encode($relations));
$result = $db->insert_or_update(
'syncroton_relations_state',
['device_id' => $device_key, 'folder_id' => $folderid, 'synctime' => $synctime],
['data'],
[$data]
);
if ($err = $db->is_error($result)) {
throw new Exception("Failed to save relation: {$err}");
}
}
}
/**
* Get state of relation objects at specified point in time
*/
protected function relations_state_get($device_key, $folderid, $synctime)
{
$synctime = $synctime->format('Y-m-d H:i:s');
//If we had a collision before
if (isset($this->relations[$folderid][$synctime . "-1"])) {
return $this->relations[$folderid][$synctime . "-1"];
}
if (!isset($this->relations[$folderid][$synctime])) {
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$db->limitquery(
"SELECT `data`, `synctime` FROM `syncroton_relations_state`"
. " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` < ?"
. " ORDER BY `synctime` DESC",
0,
1,
$device_key,
$folderid,
$synctime
);
$row = $db->fetch_assoc();
if (!$row) {
// Only if we don't have a timestamp that is older than synctime do we return the next best.
// We MUST return an entry if there are any, because otherwise we will keep INSERTing new entries in,
// relations_state_set
$db->limitquery(
"SELECT `data`, `synctime` FROM `syncroton_relations_state`"
. " WHERE `device_id` = ? AND `folder_id` = ?"
. " ORDER BY `synctime` ASC",
0,
1,
$device_key,
$folderid,
$synctime
);
$row = $db->fetch_assoc();
}
if ($row) {
- // Don't use $row['synctime'] for the internal cache.
+ // Don't use $row['synctime'] for the internal cache, and use $synctime instead.
// The synctime of the found row is usually earlier than the requested synctime.
- // Note: We use internal cache because there's a call to both hasChanges() and
+ // Note: We use the internal cache because there's a call to both hasChanges() and
// getChangedEntries() in Sync. It's needed until we add some caching on a higher level.
- $this->relations[$folderid][$synctime] = json_decode($row['data'], true);
+ $data = $row['data'];
+ $inflated = gzinflate($data)
+ // Inflation may fail for backward compatiblity
+ $data = $inflated ? $inflated : $data;
+ $this->relations[$folderid][$synctime] = json_decode($data, true);
// Cleanup: remove all records older than the current one.
// We must use the row's synctime, otherwise we would delete the record we just loaded
// We must delete all entries that are before the synctime to clean up old entries,
// but we must also delete all entries that are more recent in case the sync gets rerun
// with the same timestamp (e.g. when rerunning the same sync request).
// Otherwise the number of entries will start to grow with every sync.
$db->query(
"DELETE FROM `syncroton_relations_state`"
. " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
$device_key,
$folderid,
$row['synctime']
);
}
}
return $this->relations[$folderid][$synctime] ?? null;
}
/**
* Return last storage error
*/
public static function last_error()
{
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;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 1, 8:45 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
10075233
Default Alt Text
(73 KB)
Attached To
Mode
rS syncroton
Attached
Detach File
Event Timeline
Log In to Comment