diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist
index 6864820..2433475 100644
--- a/config/config.inc.php.dist
+++ b/config/config.inc.php.dist
@@ -1,94 +1,118 @@
/ folder exists
$config['activesync_user_debug'] = false;
// If specified all ActiveSync-related logs will be saved to this file
// Note: This doesn't change Roundcube Framework log locations
$config['activesync_log_file'] = null;
// Type of ActiveSync cache. Supported values: 'db', 'apc' and 'memcache'.
// Note: This is only for some additional data like timezones mapping.
$config['activesync_cache'] = 'db';
// lifetime of ActiveSync cache
// possible units: s, m, h, d, w
$config['activesync_cache_ttl'] = '1d';
// Type of ActiveSync Auth cache. Supported values: 'db', 'apc' and 'memcache'.
// Note: This is only for username canonification map.
$config['activesync_auth_cache'] = 'db';
// lifetime of ActiveSync Auth cache
// possible units: s, m, h, d, w
$config['activesync_auth_cache_ttl'] = '1d';
// List of global addressbooks (GAL)
// Note: If empty 'autocomplete_addressbooks' setting will be used
$config['activesync_addressbooks'] = array();
// ActiveSync => Roundcube contact fields map for GAL search
/* Default: array(
'alias' => 'nickname',
'company' => 'organization',
'displayName' => 'name',
'emailAddress' => 'email',
'firstName' => 'firstname',
'lastName' => 'surname',
'mobilePhone' => 'phone.mobile',
'office' => 'office',
'picture' => 'photo',
'phone' => 'phone',
'title' => 'jobtitle',
);
*/
$config['activesync_gal_fieldmap'] = null;
+// List of device types that will sync the LDAP addressbook(s) as a normal folder.
+// For devices that do not support GAL searching, e.g. Outlook.
+// Examples:
+// array('windowsoutlook') # enable for Oultook only
+// true # enable for all
+$config['activesync_gal_sync'] = false;
+
+// GAL cache. As reading all contacts from LDAP may be slow, caching is recommended.
+$config['activesync_gal_cache'] = 'db';
+
+// TTL of GAL cache entries. Technically this causes that synchronized
+// contacts will not be updated (queried) often than the specified interval.
+$config['activesync_gal_cache_ttl'] = '1d';
+
// List of Roundcube plugins
// WARNING: Not all plugins used in Roundcube can be listed here
$config['activesync_plugins'] = array();
// Defines for how many seconds we'll sleep between every
// action for detecting changes in folders. Default: 60
$config['activesync_ping_timeout'] = 60;
// Defines maximum Ping interval in seconds. Default: 900 (15 minutes)
$config['activesync_ping_interval'] = 900;
// We start detecting changes n seconds since the last sync of a folder
// Default: 180
$config['activesync_quiet_time'] = 180;
// When a device is reqistered, by default a set of folders are
// subscribed for syncronization, i.e. INBOX and personal folders with
// defined folder type:
// mail.drafts, mail.wastebasket, mail.sentitems, mail.outbox,
// event, event.default,
// contact, contact.default,
// task, task.default
// This default set can be extended by adding following values:
// 1 - all subscribed folders in personal namespace
// 2 - all folders in personal namespace
// 4 - all subscribed folders in other users namespace
// 8 - all folders in other users namespace
// 16 - all subscribed folders in shared namespace
// 32 - all folders in shared namespace
$config['activesync_init_subscriptions'] = 0;
// Defines blacklist of devices (device type strings) that do not support folder hierarchies.
// When set to an array folder hierarchies are used on all devices not listed here.
// When set to null an old whitelist approach will be used where we do opposite
// action and enable folder hierarchies only on device types known to support it.
$config['activesync_multifolder_blacklist'] = null;
+// Blacklist overwrites for specified object type. If set to an array
+// it will have a precedence over 'activesync_multifolder_blacklist' list only for that type.
+// Note: Outlook does not support multiple folders for contacts,
+// in that case use $config['activesync_multifolder_blacklist_contact'] = array('windowsoutlook');
+$config['activesync_multifolder_blacklist_mail'] = null;
+$config['activesync_multifolder_blacklist_event'] = null;
+$config['activesync_multifolder_blacklist_contact'] = null;
+$config['activesync_multifolder_blacklist_note'] = null;
+$config['activesync_multifolder_blacklist_task'] = null;
+
// Enables adding sender name in the From: header of send email
// when a device uses email address only (e.g. iOS devices)
$config['activesync_fix_from'] = false;
diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php
index a32e738..b03d04b 100644
--- a/lib/kolab_sync_data.php
+++ b/lib/kolab_sync_data.php
@@ -1,1822 +1,1832 @@
|
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak |
+--------------------------------------------------------------------------+
*/
/**
* Base class for Syncroton data backends
*/
abstract class kolab_sync_data implements Syncroton_Data_IData
{
/**
* ActiveSync protocol version
*
* @var int
*/
protected $asversion = 0;
/**
* information about the current device
*
* @var Syncroton_Model_IDevice
*/
protected $device;
/**
* timestamp to use for all sync requests
*
* @var DateTime
*/
protected $syncTimeStamp;
/**
* name of model to use
*
* @var string
*/
protected $modelName;
/**
* type of the default folder
*
* @var int
*/
protected $defaultFolderType;
/**
* default container for new entries
*
* @var string
*/
protected $defaultFolder;
/**
* type of user created folders
*
* @var int
*/
protected $folderType;
/**
* Internal cache for kolab_storage folder objects
*
* @var array
*/
protected $folders = array();
/**
* Internal cache for IMAP folders list
*
* @var array
*/
protected $imap_folders = array();
/**
* Timezone
*
* @var string
*/
protected $timezone;
/**
* List of device types with multiple folders support
*
* @var array
*/
protected $ext_devices = array(
'iphone',
'ipad',
'thundertine',
'windowsphone',
'wp',
'wp8',
'playbook',
);
const RESULT_OBJECT = 0;
const RESULT_UID = 1;
const RESULT_COUNT = 2;
/**
* Recurrence types
*/
const RECUR_TYPE_DAILY = 0; // Recurs daily.
const RECUR_TYPE_WEEKLY = 1; // Recurs weekly
const RECUR_TYPE_MONTHLY = 2; // Recurs monthly
const RECUR_TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day
const RECUR_TYPE_YEARLY = 5; // Recurs yearly
const RECUR_TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day
/**
* Day of week constants
*/
const RECUR_DOW_SUNDAY = 1;
const RECUR_DOW_MONDAY = 2;
const RECUR_DOW_TUESDAY = 4;
const RECUR_DOW_WEDNESDAY = 8;
const RECUR_DOW_THURSDAY = 16;
const RECUR_DOW_FRIDAY = 32;
const RECUR_DOW_SATURDAY = 64;
const RECUR_DOW_LAST = 127; // The last day of the month. Used as a special value in monthly or yearly recurrences.
/**
* Mapping of recurrence types
*
* @var array
*/
protected $recurTypeMap = array(
self::RECUR_TYPE_DAILY => 'DAILY',
self::RECUR_TYPE_WEEKLY => 'WEEKLY',
self::RECUR_TYPE_MONTHLY => 'MONTHLY',
self::RECUR_TYPE_MONTHLY_DAYN => 'MONTHLY',
self::RECUR_TYPE_YEARLY => 'YEARLY',
self::RECUR_TYPE_YEARLY_DAYN => 'YEARLY',
);
/**
* Mapping of weekdays
* NOTE: ActiveSync uses a bitmask
*
* @var array
*/
protected $recurDayMap = array(
'SU' => self::RECUR_DOW_SUNDAY,
'MO' => self::RECUR_DOW_MONDAY,
'TU' => self::RECUR_DOW_TUESDAY,
'WE' => self::RECUR_DOW_WEDNESDAY,
'TH' => self::RECUR_DOW_THURSDAY,
'FR' => self::RECUR_DOW_FRIDAY,
'SA' => self::RECUR_DOW_SATURDAY,
);
/**
* the constructor
*
* @param Syncroton_Model_IDevice $device
* @param DateTime $syncTimeStamp
*/
public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp)
{
$this->backend = kolab_sync_backend::get_instance();
$this->device = $device;
$this->asversion = floatval($device->acsversion);
$this->syncTimeStamp = $syncTimeStamp;
$this->defaultRootFolder = $this->defaultFolder . '::Syncroton';
// set internal timezone of kolab_format to user timezone
try {
$this->timezone = rcube::get_instance()->config->get('timezone', 'GMT');
kolab_format::$timezone = new DateTimeZone($this->timezone);
}
catch (Exception $e) {
//rcube::raise_error($e, true);
$this->timezone = 'GMT';
kolab_format::$timezone = new DateTimeZone('GMT');
}
}
/**
* return list of supported folders for this backend
*
* @return array
*/
public function getAllFolders()
{
$list = array();
// device supports multiple folders ?
if ($this->isMultiFolder()) {
// get the folders the user has access to
$list = $this->listFolders();
}
else if ($default = $this->getDefaultFolder()) {
$list = array($default['serverId'] => $default);
}
// getAllFolders() is called only in FolderSync
// throw Syncroton_Exception_Status_FolderSync exception
if (!is_array($list)) {
throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR);
}
foreach ($list as $idx => $folder) {
$list[$idx] = new Syncroton_Model_Folder($folder);
}
return $list;
}
/**
* Retrieve folders which were modified since last sync
*
* @param DateTime $startTimeStamp
* @param DateTime $endTimeStamp
*
* @return array List of folders
*/
public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp)
{
return array();
}
/**
* Returns true if the device supports multiple folders or it was configured so
*/
protected function isMultiFolder()
{
- $blacklist = rcube::get_instance()->config->get('activesync_multifolder_blacklist');
+ $config = rcube::get_instance()->config;
+ $blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName);
- if (is_array($blacklist)) {
- $is_multifolder = !in_array_nocase($this->device->devicetype, $blacklist);
+ if (!is_array($blacklist)) {
+ $blacklist = $config->get('activesync_multifolder_blacklist');
}
- else {
- $is_multifolder = in_array_nocase($this->device->devicetype, $this->ext_devices);
+
+ if (is_array($blacklist)) {
+ return !$this->deviceTypeFilter($blacklist);
}
- return $is_multifolder;
+ return in_array_nocase($this->device->devicetype, $this->ext_devices);
}
/**
* Returns default folder for current class type.
*/
protected function getDefaultFolder()
{
// Check if there's any folder configured for sync
$folders = $this->listFolders();
if (empty($folders)) {
return $folders;
}
foreach ($folders as $folder) {
if ($folder['type'] == $this->defaultFolderType) {
$default = $folder;
break;
}
}
// Return first on the list if there's no default
if (empty($default)) {
$key = array_shift(array_keys($folders));
$default = $folders[$key];
// make sure the type is default here
$default['type'] = $this->defaultFolderType;
}
// Remember real folder ID and set ID/name to root folder
$default['realid'] = $default['serverId'];
$default['serverId'] = $this->defaultRootFolder;
$default['displayName'] = $this->defaultFolder;
return $default;
}
/**
* Creates a folder
*/
public function createFolder(Syncroton_Model_IFolder $folder)
{
$parentid = $folder->parentId;
$type = $folder->type;
$display_name = $folder->displayName;
if ($parentid) {
$parent = $this->backend->folder_id2name($parentid, $this->device->deviceid);
if ($parent === null) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND);
}
}
$name = rcube_charset::convert($display_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parent !== null) {
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
// Create IMAP folder
$result = $this->backend->folder_create($name, $type, $this->device->deviceid);
if ($result) {
$folder->serverId = $this->backend->folder_id($name);
return $folder;
}
$errno = Syncroton_Exception_Status_FolderCreate::UNKNOWN_ERROR;
// Special case when client tries to create a subfolder of INBOX
// which is not possible on Cyrus-IMAP (T2223)
if ($parent == 'INBOX' && stripos($this->backend->last_error(), 'invalid') !== false) {
$errno = Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER;
}
// Note: Looks like Outlook 2013 ignores any errors on FolderCreate command
throw new Syncroton_Exception_Status_FolderCreate($errno);
}
/**
* Updates a folder
*/
public function updateFolder(Syncroton_Model_IFolder $folder)
{
$parentid = $folder->parentId;
$type = $folder->type;
$display_name = $folder->displayName;
$old_name = $this->backend->folder_id2name($folder->serverId, $this->device->deviceid);
if ($parentid) {
$parent = $this->backend->folder_id2name($parentid, $this->device->deviceid);
}
$name = rcube_charset::convert($display_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parent !== null) {
$rcube = rcube::get_instance();
$storage = $rcube->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
// Rename/move IMAP folder
if ($name == $old_name) {
$result = true;
// @TODO: folder type change?
}
else {
$result = $this->backend->folder_rename($old_name, $name, $type);
}
if ($result) {
return $folder;
}
// @TODO: throw exception
}
/**
* Deletes a folder
*/
public function deleteFolder($folder)
{
if ($folder instanceof Syncroton_Model_IFolder) {
$folder = $folder->serverId;
}
$name = $this->backend->folder_id2name($folder, $this->device->deviceid);
// @TODO: throw exception
return $this->backend->folder_delete($name, $this->device->deviceid);
}
/**
* Empty folder (remove all entries and optionally subfolders)
*
* @param string $folderId Folder identifier
* @param array $options Options
*/
public function emptyFolderContents($folderid, $options)
{
$folders = $this->extractFolders($folderid);
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
// Remove all entries
$folder->delete_all();
// Remove subfolders
if (!empty($options['deleteSubFolders'])) {
$list = $this->listFolders($folderid);
foreach ($list as $folderid => $folder) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
// Remove all entries
$folder->delete_all();
}
}
}
}
/**
* Moves object into another location (folder)
*
* @param string $srcFolderId Source folder identifier
* @param string $serverId Object identifier
* @param string $dstFolderId Destination folder identifier
*
* @throws Syncroton_Exception_Status
* @return string New object identifier
*/
public function moveItem($srcFolderId, $serverId, $dstFolderId)
{
$item = $this->getObject($srcFolderId, $serverId, $folder);
if (!$item || !$folder) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
$dstname = $this->backend->folder_id2name($dstFolderId, $this->device->deviceid);
if ($dstname === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if (!$folder->move($serverId, $dstname)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
return $item['uid'];
}
/**
* Add entry
*
* @param string $folderId Folder identifier
* @param Syncroton_Model_IEntry $entry Entry object
*
* @return string ID of the created entry
*/
public function createEntry($folderId, Syncroton_Model_IEntry $entry)
{
$entry = $this->toKolab($entry, $folderId);
$entry = $this->createObject($folderId, $entry);
if (empty($entry)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $entry['uid'];
}
/**
* update existing entry
*
* @param string $folderId
* @param string $serverId
* @param SimpleXMLElement $entry
*
* @return string ID of the updated entry
*/
public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
{
$oldEntry = $this->getObject($folderId, $serverId);
if (empty($oldEntry)) {
throw new Syncroton_Exception_NotFound('id not found');
}
$entry = $this->toKolab($entry, $folderId, $oldEntry);
$entry = $this->updateObject($folderId, $serverId, $entry);
if (empty($entry)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $entry['uid'];
}
/**
* delete entry
*
* @param string $folderId
* @param string $serverId
* @param array $collectionData
*/
public function deleteEntry($folderId, $serverId, $collectionData)
{
$deleted = $this->deleteObject($folderId, $serverId);
if (!$deleted) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
}
public function getFileReference($fileReference)
{
// to be implemented by Email data class
// @TODO: throw "unimplemented" exception here?
}
/**
* Search for existing entries
*
* @param string $folderid Folder identifier
* @param array $filter Search filter
* @param int $result_type Type of the result (see RESULT_* constants)
*
* @return array|int Search result as count or array of uids/objects
*/
protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID)
{
if ($folderid == $this->defaultRootFolder) {
$folders = $this->listFolders();
if (!is_array($folders)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$folders = array_keys($folders);
}
else {
$folders = array($folderid);
}
// there's a PHP Warning from kolab_storage if $filter isn't an array
if (empty($filter)) {
$filter = array();
}
else {
$changed_objects = $this->getChangesByRelations($folderid, $filter);
}
$result = $result_type == self::RESULT_COUNT ? 0 : array();
$found = 0;
foreach ($folders as $folder_id) {
$foldername = $this->backend->folder_id2name($folder_id, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$found++;
$error = false;
switch ($result_type) {
case self::RESULT_COUNT:
$count = $folder->count($filter);
if ($count === null || $count === false) {
$error = true;
}
else {
$result += (int) $count;
}
break;
case self::RESULT_UID:
$uids = $folder->get_uids($filter);
if (!is_array($uids)) {
$error = true;
}
else {
$result = array_merge($result, $uids);
}
break;
}
if ($error) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
// 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 = array(array('uid', '=', $changed_objects));
foreach ($filter as $f) {
if ($f[0] != 'changed') {
$tag_filter[] = $f;
}
}
switch ($result_type) {
case self::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 && $count !== false) {
$result += (int) $count;
}
break;
case self::RESULT_UID:
$uids = $folder->get_uids($tag_filter);
if (is_array($uids)) {
$result = array_unique(array_merge($result, $uids));
}
break;
}
}
}
if (!$found) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
return $result;
}
/**
* Detect changes of relation (tag) objects data and assigned objects
* Returns relation member identifiers
*/
protected function getChangesByRelations($folderid, $filter)
{
if (!$this->tag_categories) {
return;
}
// 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->backend->relations_state_get($this->device->id, $folderid, $since);
// get current relations state
$config = kolab_storage_config::get_instance();
$default = true;
$filter = array(
array('type', '=', 'relation'),
array('category', '=', 'tag')
);
$relations = $config->get_objects($filter, $default, 100);
$result = array();
$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...
else if ($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...
else if ($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 = array();
foreach ($relations as $relation) {
$data[$relation['uid']] = array(
'name' => $relation['name'],
'changed' => $relation['changed']->format('U'),
'members' => implode("\n", $relation['members']),
);
}
$now = new DateTime('now', new DateTimeZone('UTC'));
$this->backend->relations_state_set($this->device->id, $folderid, $now, $data);
}
// in mail mode return only message URIs
if ($this->modelName == 'mail') {
// 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;
}
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
// overwrite by child class according to specified type
return array();
}
/**
* get all entries changed between two dates
*
* @param string $folderId
* @param DateTime $start
* @param DateTime $end
* @param int $filterType
*
* @return array
*/
public function getChangedEntries($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
{
$filter = $this->filter($filter_type);
$filter[] = array('changed', '>', $start);
if ($end) {
$filter[] = array('changed', '<=', $end);
}
return $this->searchEntries($folderId, $filter, self::RESULT_UID);
}
/**
* Get count of entries changed between two dates
*
* @param string $folderId
* @param DateTime $start
* @param DateTime $end
* @param int $filterType
*
* @return int
*/
public function getChangedEntriesCount($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
{
$filter = $this->filter($filter_type);
$filter[] = array('changed', '>', $start);
if ($end) {
$filter[] = array('changed', '<=', $end);
}
return $this->searchEntries($folderId, $filter, self::RESULT_COUNT);
}
/**
* get id's of all entries available on the server
*
* @param string $folderId
* @param int $filterType
*
* @return array
*/
public function getServerEntries($folder_id, $filter_type)
{
$filter = $this->filter($filter_type);
$result = $this->searchEntries($folder_id, $filter, self::RESULT_UID);
return $result;
}
/**
* get count of all entries available on the server
*
* @param string $folderId
* @param int $filterType
*
* @return int
*/
public function getServerEntriesCount($folder_id, $filter_type)
{
$filter = $this->filter($filter_type);
$result = $this->searchEntries($folder_id, $filter, self::RESULT_COUNT);
return $result;
}
/**
* Returns number of changed objects in the backend folder
*
* @param Syncroton_Backend_IContent $contentBackend
* @param Syncroton_Model_IFolder $folder
* @param Syncroton_Model_ISyncState $syncState
*
* @return int
*/
public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
$allClientEntries = $contentBackend->getFolderState($this->device, $folder);
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$changedEntries = $this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
return count($addedEntries) + count($deletedEntries) + $changedEntries;
}
/**
* Returns true if any data got modified in the backend folder
*
* @param Syncroton_Backend_IContent $contentBackend
* @param Syncroton_Model_IFolder $folder
* @param Syncroton_Model_ISyncState $syncState
*
* @return bool
*/
public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
- // Try to detect change in multi-folder mode and throw exception
- // so device will re-sync folders hierarchy
- // @TODO: this is a temp solution until we have real hierarchy
- // changes detection fort Ping/Hartbeat
- $is_multifolder = $this->isMultiFolder();
- if (($is_multifolder && $folder->serverId == $this->defaultRootFolder)
- || (!$is_multifolder && $folder->type >= 12)
- ) {
- throw new Syncroton_Exception_NotFound('Folder not found');
- }
-
try {
if ($this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype)) {
return true;
}
$allClientEntries = $contentBackend->getFolderState($this->device, $folder);
// @TODO: Consider looping over all folders here, not in getServerEntries() and
// getChangedEntriesCount(). This way we could break the loop and not check all folders
// or at least skip redundant cache sync of the same folder
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
return count($addedEntries) > 0 || count($deletedEntries) > 0;
}
catch (Exception $e) {
// return "no changes" if something failed
return false;
}
}
/**
* Fetches the entry from the backend
*/
protected function getObject($folderid, $entryid, &$folder = null)
{
$folders = $this->extractFolders($folderid);
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if ($folder && $folder->valid && ($object = $folder->get_object($entryid))) {
$object['_folderid'] = $folderid;
return $object;
}
}
}
/**
* Saves the entry on the backend
*/
protected function createObject($folderid, $data)
{
if ($folderid == $this->defaultRootFolder) {
$default = $this->getDefaultFolder();
if (!is_array($default)) {
return null;
}
$folderid = isset($default['realid']) ? $default['realid'] : $default['serverId'];
}
// convert categories into tags, save them after creating an object
if ($this->tag_categories) {
$tags = $data['categories'];
unset($data['categories']);
}
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
$folder = $this->getFolderObject($foldername);
if ($folder && $folder->valid && $folder->save($data)) {
if (!empty($tags)) {
$this->setKolabTags($data['uid'], $tags);
}
return $data;
}
}
/**
* Updates the entry on the backend
*/
protected function updateObject($folderid, $entryid, $data)
{
$object = $this->getObject($folderid, $entryid);
if ($object) {
$folder = $this->getFolderObject($object['_mailbox']);
// convert categories into tags, save them after updating an object
if ($this->tag_categories && array_key_exists('categories', $data)) {
$tags = (array) $data['categories'];
unset($data['categories']);
}
if ($folder && $folder->valid && $folder->save($data)) {
if (isset($tags)) {
$this->setKolabTags($data['uid'], $tags);
}
return $data;
}
}
}
/**
* Removes the entry from the backend
*/
protected function deleteObject($folderid, $entryid)
{
$object = $this->getObject($folderid, $entryid);
if ($object) {
$folder = $this->getFolderObject($object['_mailbox']);
if ($folder && $folder->valid && $folder->delete($entryid)) {
if ($this->tag_categories) {
$this->setKolabTags($object['uid'], null);
}
return true;
}
return false;
}
// object doesn't exist, confirm deletion
return true;
}
/**
* Returns internal folder IDs
*
* @param string $folderid Folder identifier
*
* @return array List of folder identifiers
*/
protected function extractFolders($folderid)
{
if ($folderid instanceof Syncroton_Model_IFolder) {
$folderid = $folderid->serverId;
}
if ($folderid == $this->defaultRootFolder) {
$folders = $this->listFolders();
if (!is_array($folders)) {
return null;
}
$folders = array_keys($folders);
}
else {
$folders = array($folderid);
}
return $folders;
}
/**
* List of all IMAP folders (or subtree)
*
* @param string $parentid Parent folder identifier
*
* @return array List of folder identifiers
*/
protected function listFolders($parentid = null)
{
if (empty($this->imap_folders)) {
$this->imap_folders = $this->backend->folders_list(
$this->device->deviceid, $this->modelName, $this->isMultiFolder());
}
if ($parentid === null) {
return $this->imap_folders;
}
$folders = array();
$parents = array($parentid);
foreach ($this->imap_folders as $folder_id => $folder) {
if ($folder['parentId'] && in_array($folder['parentId'], $parents)) {
$folders[$folder_id] = $folder;
$parents[] = $folder_id;
}
}
return $folders;
}
/**
* Returns Folder object (uses internal cache)
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return kolab_storage_folder Folder object
*/
protected function getFolderObject($name)
{
if ($name === null || $name === '') {
return null;
}
if (!isset($this->folders[$name])) {
$this->folders[$name] = kolab_storage::get_folder($name, $this->modelName);
}
return $this->folders[$name];
}
/**
* Returns ActiveSync settings of specified folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return array Folder settings
*/
protected function getFolderConfig($name)
{
$metadata = $this->backend->folder_meta();
if (!is_array($metadata)) {
return array();
}
$deviceid = $this->device->deviceid;
$config = $metadata[$name]['FOLDER'][$deviceid];
return array(
'ALARMS' => $config['S'] == 2,
);
}
/**
* Returns real folder name for specified folder ID
*/
protected function getFolderName($folderid)
{
if ($folderid == $this->defaultRootFolder) {
$default = $this->getDefaultFolder();
if (!is_array($default)) {
return null;
}
$folderid = isset($default['realid']) ? $default['realid'] : $default['serverId'];
}
return $this->backend->folder_id2name($folderid, $this->device->deviceid);
}
/**
* Convert contact from xml to kolab format
*
* @param Syncroton_Model_IEntry $data Contact data
* @param string $folderId Folder identifier
* @param array $entry Old Contact data for merge
*
* @return array
*/
abstract function toKolab(Syncroton_Model_IEntry $data, $folderId, $entry = null);
/**
* Extracts data from kolab data array
*/
protected function getKolabDataItem($data, $name)
{
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!empty($data[$name]) && is_array($data[$name])) {
foreach ($data[$name] as $element) {
if ($element['type'] == $type) {
return $element[$key_name];
}
}
}
return null;
}
/*
// hash array e.g. organizer
else if ($count == 2) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!empty($data[$name]) && is_array($data[$name])) {
foreach ($data[$name] as $element) {
if ($element['type'] == $type) {
return $element[$key_name];
}
}
}
return null;
}
*/
$name_items = explode(':', $name);
$name = $name_items[0];
if (empty($data[$name])) {
return null;
}
// simple array (e.g. email)
if (count($name_items) == 2) {
return $data[$name][$name_items[1]];
}
return $data[$name];
}
/**
* Saves data in kolab data array
*/
protected function setKolabDataItem(&$data, $name, $value)
{
if (empty($value)) {
return $this->unsetKolabDataItem($data, $name);
}
$name_items = explode('.', $name);
// multi-level array (e.g. address, phone)
if (count($name_items) == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!isset($data[$name])) {
$data[$name] = array();
}
foreach ($data[$name] as $idx => $element) {
if ($element['type'] == $type) {
$found = $idx;
break;
}
}
if (!isset($found)) {
$data[$name] = array_values($data[$name]);
$found = count($data[$name]);
$data[$name][$found] = array('type' => $type);
}
$data[$name][$found][$key_name] = $value;
return;
}
$name_items = explode(':', $name);
$name = $name_items[0];
// simple array (e.g. email)
if (count($name_items) == 2) {
$data[$name][$name_items[1]] = $value;
return;
}
$data[$name] = $value;
}
/**
* Unsets data item in kolab data array
*/
protected function unsetKolabDataItem(&$data, $name)
{
$name_items = explode('.', $name);
// multi-level array (e.g. address, phone)
if (count($name_items) == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!isset($data[$name])) {
return;
}
foreach ($data[$name] as $idx => $element) {
if ($element['type'] == $type) {
$found = $idx;
break;
}
}
if (!isset($found)) {
return;
}
unset($data[$name][$found][$key_name]);
// if there's only one element and it's 'type', remove it
if (count($data[$name][$found]) == 1 && isset($data[$name][$found]['type'])) {
unset($data[$name][$found]['type']);
}
if (empty($data[$name][$found])) {
unset($data[$name][$found]);
}
if (empty($data[$name])) {
unset($data[$name]);
}
return;
}
$name_items = explode(':', $name);
$name = $name_items[0];
// simple array (e.g. email)
if (count($name_items) == 2) {
unset($data[$name][$name_items[1]]);
if (empty($data[$name])) {
unset($data[$name]);
}
return;
}
unset($data[$name]);
}
/**
* Setter for Body attribute according to client version
*
* @param string $value Body
* @param array $param Body parameters
*
* @reurn Syncroton_Model_EmailBody Body element
*/
protected function setBody($value, $params = array())
{
if (empty($value) && empty($params)) {
return;
}
// Old protocol version doesn't support AirSyncBase:Body, it's eg. WindowsCE
if ($this->asversion < 12) {
return;
}
if (!empty($value)) {
// cast to string to workaround issue described in Bug #1635
$params['data'] = (string) $value;
}
if (!isset($params['type'])) {
$params['type'] = Syncroton_Model_EmailBody::TYPE_PLAINTEXT;
}
return new Syncroton_Model_EmailBody($params);
}
/**
* Getter for Body attribute value according to client version
*
* @param mixed $body Body element
* @param int $type Result data type (to which the body will be converted, if specified).
* One or array of Syncroton_Model_EmailBody constants.
*
* @return string Body value
*/
protected function getBody($body, $type = null)
{
if ($body && $body->data) {
$data = $body->data;
}
if (!$data || empty($type)) {
return;
}
$type = (array) $type;
// Convert to specified type
if (!in_array($body->type, $type)) {
$converter = new kolab_sync_body_converter($data, $body->type);
$data = $converter->convert($type[0]);
}
return $data;
}
/**
* Converts text (plain or html) into ActiveSync Body element.
* Takes bodyPreferences into account and detects if the text is plain or html.
*/
protected function body_from_kolab($body, $collection)
{
if (empty($body)) {
return;
}
$opts = $collection->options;
$prefs = $opts['bodyPreferences'];
$html_type = Syncroton_Command_Sync::BODY_TYPE_HTML;
$type = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
$params = array();
// HTML? check for opening and closing or tags
$is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, ''.$m[1].'>') > 0;
// here we assume that all devices support plain text
if ($is_html) {
// device supports HTML...
if (!empty($prefs[$html_type])) {
$type = $html_type;
}
// ...else convert to plain text
else {
$txt = new rcube_html2text($body, false, true);
$body = $txt->get_text();
}
}
// strip out any non utf-8 characters
$body = rcube_charset::clean($body);
$real_length = $body_length = strlen($body);
// truncate the body if needed
if (($truncateAt = $prefs[$type]['truncationSize']) && $body_length > $truncateAt) {
$body = mb_strcut($body, 0, $truncateAt);
$body_length = strlen($body);
$params['truncated'] = 1;
$params['estimatedDataSize'] = $real_length;
}
$params['type'] = $type;
return $this->setBody($body, $params);
}
/**
* Converts PHP DateTime, date (YYYY-MM-DD) or unixtimestamp into PHP DateTime in UTC
*
* @param DateTime|int|string $date Unix timestamp, date (YYYY-MM-DD) or PHP DateTime object
*
* @return DateTime Datetime object
*/
protected static function date_from_kolab($date)
{
if (!empty($date)) {
if (is_numeric($date)) {
$date = new DateTime('@' . $date);
}
else if (is_string($date)) {
$date = new DateTime($date, new DateTimeZone('UTC'));
}
else if ($date instanceof DateTime) {
$date = clone $date;
$tz = $date->getTimezone();
$tz_name = $tz->getName();
// convert to UTC if needed
if ($tz_name != 'UTC') {
$utc = new DateTimeZone('UTC');
// safe dateonly object conversion to UTC
// note: _dateonly flag is set by libkolab e.g. for birthdays
if ($date->_dateonly) {
// avoid time change
$date = new DateTime($date->format('Y-m-d'), $utc);
// set time to noon to avoid timezone troubles
$date->setTime(12, 0, 0);
}
else {
$date->setTimezone($utc);
}
}
}
else {
return null; // invalid input
}
return $date;
}
}
/**
* Convert Kolab event/task recurrence into ActiveSync
*/
protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event')
{
if (empty($data['recurrence'])) {
return;
}
$recurrence = array();
$r = $data['recurrence'];
// required fields
switch($r['FREQ']) {
case 'DAILY':
$recurrence['type'] = self::RECUR_TYPE_DAILY;
break;
case 'WEEKLY':
$recurrence['type'] = self::RECUR_TYPE_WEEKLY;
$recurrence['dayOfWeek'] = $this->day2bitmask($r['BYDAY']);
break;
case 'MONTHLY':
if (!empty($r['BYMONTHDAY'])) {
// @TODO: ActiveSync doesn't support multi-valued month days,
// should we replicate the recurrence element for each day of month?
$month_day = array_shift(explode(',', $r['BYMONTHDAY']));
$recurrence['type'] = self::RECUR_TYPE_MONTHLY;
$recurrence['dayOfMonth'] = $month_day;
}
else {
$week = (int) substr($r['BYDAY'], 0, -2);
$week = ($week == -1) ? 5 : $week;
$day = substr($r['BYDAY'], -2);
$recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN;
$recurrence['weekOfMonth'] = $week;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
}
break;
case 'YEARLY':
// @TODO: ActiveSync doesn't support multi-valued months,
// should we replicate the recurrence element for each month?
$month = array_shift(explode(',', $r['BYMONTH']));
if (!empty($r['BYDAY'])) {
$week = (int) substr($r['BYDAY'], 0, -2);
$week = ($week == -1) ? 5 : $week;
$day = substr($r['BYDAY'], -2);
$recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN;
$recurrence['weekOfMonth'] = $week;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
$recurrence['monthOfYear'] = $month;
}
else if (!empty($r['BYMONTHDAY'])) {
// @TODO: ActiveSync doesn't support multi-valued month days,
// should we replicate the recurrence element for each day of month?
$month_day = array_shift(explode(',', $r['BYMONTHDAY']));
$recurrence['type'] = self::RECUR_TYPE_YEARLY;
$recurrence['dayOfMonth'] = $month_day;
$recurrence['monthOfYear'] = $month;
}
else {
$recurrence['type'] = self::RECUR_TYPE_YEARLY;
$recurrence['monthOfYear'] = $month;
}
break;
}
// required field
$recurrence['interval'] = $r['INTERVAL'] ? $r['INTERVAL'] : 1;
if (!empty($r['UNTIL'])) {
$recurrence['until'] = self::date_from_kolab($r['UNTIL']);
}
else if (!empty($r['COUNT'])) {
$recurrence['occurrences'] = $r['COUNT'];
}
$class = 'Syncroton_Model_' . $type . 'Recurrence';
$result['recurrence'] = new $class($recurrence);
// Tasks do not support exceptions
if ($type == 'Event') {
$result['exceptions'] = $this->exceptions_from_kolab($collection, $data);
}
}
/**
* Convert ActiveSync event/task recurrence into Kolab
*/
protected function recurrence_to_kolab($data, $folderid, $timezone = null)
{
if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence) || !isset($data->recurrence->type)) {
return null;
}
$recurrence = $data->recurrence;
$type = $recurrence->type;
switch ($type) {
case self::RECUR_TYPE_DAILY:
break;
case self::RECUR_TYPE_WEEKLY:
$rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek);
break;
case self::RECUR_TYPE_MONTHLY:
$rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
break;
case self::RECUR_TYPE_MONTHLY_DAYN:
$week = $recurrence->weekOfMonth;
$day = $recurrence->dayOfWeek;
$byDay = $week == 5 ? -1 : $week;
$byDay .= $this->bitmask2day($day);
$rrule['BYDAY'] = $byDay;
break;
case self::RECUR_TYPE_YEARLY:
$rrule['BYMONTH'] = $recurrence->monthOfYear;
$rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
break;
case self::RECUR_TYPE_YEARLY_DAYN:
$rrule['BYMONTH'] = $recurrence->monthOfYear;
$week = $recurrence->weekOfMonth;
$day = $recurrence->dayOfWeek;
$byDay = $week == 5 ? -1 : $week;
$byDay .= $this->bitmask2day($day);
$rrule['BYDAY'] = $byDay;
break;
}
$rrule['FREQ'] = $this->recurTypeMap[$type];
$rrule['INTERVAL'] = isset($recurrence->interval) ? $recurrence->interval : 1;
if (isset($recurrence->until)) {
if ($timezone) {
$recurrence->until->setTimezone($timezone);
}
$rrule['UNTIL'] = $recurrence->until;
}
else if (!empty($recurrence->occurrences)) {
$rrule['COUNT'] = $recurrence->occurrences;
}
// recurrence exceptions (not supported by Tasks)
if ($data instanceof Syncroton_Model_Event) {
$this->exceptions_to_kolab($data, $rrule, $folderid, $timezone);
}
return $rrule;
}
/**
* Convert Kolab event recurrence exceptions into ActiveSync
*/
protected function exceptions_from_kolab($collection, $data)
{
if (empty($data['recurrence']['EXCEPTIONS']) && empty($data['recurrence']['EXDATE'])) {
return null;
}
$ex_list = array();
// exceptions (modified occurences)
foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) {
$exception['_mailbox'] = $data['_mailbox'];
$ex = $this->getEntry($collection, $exception, true);
$ex['exceptionStartTime'] = clone $ex['startTime'];
// remove fields not supported by Syncroton_Model_EventException
unset($ex['uID']);
// @TODO: 'thisandfuture=true' is not supported in Activesync
// we'd need to slit the event into two separate events
$ex_list[] = new Syncroton_Model_EventException($ex);
}
// exdate (deleted occurences)
foreach ((array)$data['recurrence']['EXDATE'] as $exception) {
if (!($exception instanceof DateTime)) {
continue;
}
// set event start time to exception date
// that can't be any time, tested with Android
$hour = $data['_start']->format('H');
$minute = $data['_start']->format('i');
$second = $data['_start']->format('s');
$exception->setTime($hour, $minute, $second);
$exception->_dateonly = false;
$ex = array(
'deleted' => 1,
'exceptionStartTime' => self::date_from_kolab($exception),
);
$ex_list[] = new Syncroton_Model_EventException($ex);
}
return $ex_list;
}
/**
* Convert ActiveSync event recurrence exceptions into Kolab
*/
protected function exceptions_to_kolab($data, &$rrule, $folderid, $timezone = null)
{
$rrule['EXDATE'] = array();
$rrule['EXCEPTIONS'] = array();
// handle exceptions from recurrence
if (!empty($data->exceptions)) {
foreach ($data->exceptions as $exception) {
if ($exception->deleted) {
$date = clone $exception->exceptionStartTime;
if ($timezone) {
$date->setTimezone($timezone);
}
$date->setTime(0, 0, 0);
$rrule['EXDATE'][] = $date;
}
else if (!$exception->deleted) {
$ex = $this->toKolab($exception, $folderid, null, $timezone);
if ($data->allDayEvent) {
$ex['allday'] = 1;
}
$rrule['EXCEPTIONS'][] = $ex;
}
}
}
if (empty($rrule['EXDATE'])) {
unset($rrule['EXDATE']);
}
if (empty($rrule['EXCEPTIONS'])) {
unset($rrule['EXCEPTIONS']);
}
}
/**
* Returns list of tag names assigned to kolab object
*/
protected function getKolabTags($uid, $categories = null)
{
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($uid);
$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;
}
/**
* Set tags to kolab object
*/
protected function setKolabTags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Converts string of days (TU,TH) to bitmask used by ActiveSync
*
* @param string $days
*
* @return int
*/
protected function day2bitmask($days)
{
$days = explode(',', $days);
$result = 0;
foreach ($days as $day) {
$result = $result + $this->recurDayMap[$day];
}
return $result;
}
/**
* Convert bitmask used by ActiveSync to string of days (TU,TH)
*
* @param int $days
*
* @return string
*/
protected function bitmask2day($days)
{
$days_arr = array();
for ($bitmask = 1; $bitmask <= self::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) {
$dayMatch = $days & $bitmask;
if ($dayMatch === $bitmask) {
$days_arr[] = array_search($bitmask, $this->recurDayMap);
}
}
$result = implode(',', $days_arr);
return $result;
}
+
+ /**
+ * Check if current device type string matches any of options
+ */
+ protected function deviceTypeFilter($options)
+ {
+ foreach ($options as $option) {
+ if ($option[0] == '/') {
+ if (preg_match($option, $this->device->devicetype)) {
+ return true;
+ }
+ }
+ else if (stripos($this->device->devicetype, $option) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/lib/kolab_sync_data_contacts.php b/lib/kolab_sync_data_contacts.php
index e348474..0562377 100644
--- a/lib/kolab_sync_data_contacts.php
+++ b/lib/kolab_sync_data_contacts.php
@@ -1,283 +1,637 @@
|
+ | Copyright (C) 2011-2017, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak |
+--------------------------------------------------------------------------+
*/
/**
* COntacts data class for Syncroton
*/
class kolab_sync_data_contacts extends kolab_sync_data
{
/**
* Mapping from ActiveSync Contacts namespace fields
*/
protected $mapping = array(
'anniversary' => 'anniversary',
'assistantName' => 'assistant:0',
//'assistantPhoneNumber' => 'assistantphonenumber',
'birthday' => 'birthday',
'body' => 'notes',
'businessAddressCity' => 'address.work.locality',
'businessAddressCountry' => 'address.work.country',
'businessAddressPostalCode' => 'address.work.code',
'businessAddressState' => 'address.work.region',
'businessAddressStreet' => 'address.work.street',
'businessFaxNumber' => 'phone.workfax.number',
'businessPhoneNumber' => 'phone.work.number',
'carPhoneNumber' => 'phone.car.number',
//'categories' => 'categories',
'children' => 'children',
'companyName' => 'organization',
'department' => 'department',
//'email1Address' => 'email:0',
//'email2Address' => 'email:1',
//'email3Address' => 'email:2',
//'fileAs' => 'fileas', //@TODO: ?
'firstName' => 'firstname',
//'home2PhoneNumber' => 'home2phonenumber',
'homeAddressCity' => 'address.home.locality',
'homeAddressCountry' => 'address.home.country',
'homeAddressPostalCode' => 'address.home.code',
'homeAddressState' => 'address.home.region',
'homeAddressStreet' => 'address.home.street',
'homeFaxNumber' => 'phone.homefax.number',
'homePhoneNumber' => 'phone.home.number',
'jobTitle' => 'jobtitle',
'lastName' => 'surname',
'middleName' => 'middlename',
'mobilePhoneNumber' => 'phone.mobile.number',
//'officeLocation' => 'officelocation',
'otherAddressCity' => 'address.office.locality',
'otherAddressCountry' => 'address.office.country',
'otherAddressPostalCode' => 'address.office.code',
'otherAddressState' => 'address.office.region',
'otherAddressStreet' => 'address.office.street',
'pagerNumber' => 'phone.pager.number',
'picture' => 'photo',
//'radioPhoneNumber' => 'radiophonenumber',
//'rtf' => 'rtf',
'spouse' => 'spouse',
'suffix' => 'suffix',
'title' => 'prefix',
'webPage' => 'website.homepage.url',
//'yomiCompanyName' => 'yomicompanyname',
//'yomiFirstName' => 'yomifirstname',
//'yomiLastName' => 'yomilastname',
// Mapping from ActiveSync Contacts2 namespace fields
//'accountName' => 'accountname',
//'companyMainPhone' => 'companymainphone',
//'customerId' => 'customerid',
//'governmentId' => 'governmentid',
'iMAddress' => 'im:0',
'iMAddress2' => 'im:1',
'iMAddress3' => 'im:2',
'managerName' => 'manager:0',
//'mMS' => 'mms',
'nickName' => 'nickname',
);
/**
* Kolab object type
*
* @var string
*/
protected $modelName = 'contact';
/**
* Type of the default folder
*
* @var int
*/
protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT;
/**
* Default container for new entries
*
* @var string
*/
protected $defaultFolder = 'Contacts';
/**
* Type of user created folders
*
* @var int
*/
protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED;
+ /**
+ * Identifier of special Global Address List folder
+ *
+ * @var string
+ */
+ protected $galFolder = 'GAL';
+
+ /**
+ * Name of special Global Address List folder
+ *
+ * @var string
+ */
+ protected $galFolderName = 'Global Address Book';
+
+ protected $galPrefix = 'GAL:';
+ protected $galSources;
+ protected $galResult;
+ protected $galCache;
+
/**
* Creates model object
*
* @param Syncroton_Model_SyncCollection $collection Collection data
* @param string $serverId Local entry identifier
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId)
{
$data = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId);
$result = array();
+ if (empty($data)) {
+ throw new Syncroton_Exception_NotFound("Contact $serverId not found");
+ }
+
// Contacts namespace fields
foreach ($this->mapping as $key => $name) {
$value = $this->getKolabDataItem($data, $name);
switch ($name) {
case 'photo':
if ($value) {
// ActiveSync limits photo size to 48KB (of base64 encoded string)
if (strlen($value) * 1.33 > 48 * 1024) {
continue;
}
}
break;
case 'birthday':
case 'anniversary':
$value = self::date_from_kolab($value);
break;
case 'notes':
$value = $this->body_from_kolab($value, $collection);
break;
}
if (empty($value) || is_array($value)) {
continue;
}
$result[$key] = $value;
}
// email address(es): email1Address, email2Address, email3Address
for ($x=0; $x<3; $x++) {
- if (!empty($data['email'][$x]) && !empty($data['email'][$x]['address'])) {
- $result['email' . ($x+1) . 'Address'] = $data['email'][$x]['address'];
+ if ($email = $data['email'][$x]) {
+ if (is_array($email)) {
+ $email = $email['address'];
+ }
+ if ($email) {
+ $result['email' . ($x+1) . 'Address'] = $email;
+ }
}
}
return new Syncroton_Model_Contact($result);
}
/**
* convert contact from xml to libkolab array
*
* @param Syncroton_Model_IEntry $data Contact to convert
* @param string $folderId Folder identifier
* @param array $entry Existing entry
*
* @return array Kolab object array
*/
public function toKolab(Syncroton_Model_IEntry $data, $folderId, $entry = null)
{
$contact = !empty($entry) ? $entry : array();
// Contacts namespace fields
foreach ($this->mapping as $key => $name) {
$value = $data->$key;
switch ($name) {
case 'address.work.street':
if (strtolower($this->device->devicetype) == 'palm') {
// palm pre sends the whole address in the tag
$value = null;
}
break;
case 'website.homepage.url':
// remove facebook urls
if (preg_match('/^fb:\/\//', $value)) {
$value = null;
}
break;
case 'notes':
$value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT);
// If note isn't specified keep old note
if ($value === null) {
continue 2;
}
break;
case 'photo':
// If photo isn't specified keep old photo
if ($value === null) {
continue 2;
}
break;
case 'birthday':
case 'anniversary':
if ($value) {
// convert date to string format, so libkolab will store
// it with no time and timezone what could be incorrectly re-calculated (#2555)
$value = $value->format('Y-m-d');
}
break;
}
$this->setKolabDataItem($contact, $name, $value);
}
// email address(es): email1Address, email2Address, email3Address
$emails = array();
for ($x=0; $x<3; $x++) {
$key = 'email' . ($x+1) . 'Address';
if ($value = $data->$key) {
// Android sends email address as: Lars Kneschke
if (preg_match('/(.*)<(.+@[^@]+)>/', $value, $matches)) {
$value = trim($matches[2]);
}
// sanitize email address, it can contain broken (non-unicode) characters (#3287)
$value = rcube_charset::clean($value);
// try to find address type, at least we can do this if
// address wasn't changed
$type = '';
foreach ((array)$contact['email'] as $email) {
if ($email['address'] == $value) {
$type = $email['type'];
}
}
$emails[] = array('address' => $value, 'type' => $type);
}
}
$contact['email'] = $emails;
return $contact;
}
+ /**
+ * Return list of supported folders for this backend
+ *
+ * @return array
+ */
+ public function getAllFolders()
+ {
+ $list = parent::getAllFolders();
+
+ if ($this->isMultiFolder() && $this->hasGAL()) {
+ $list[$this->galFolder] = new Syncroton_Model_Folder(array(
+ 'displayName' => $this->galFolderName, // @TODO: localization?
+ 'serverId' => $this->galFolder,
+ 'parentId' => 0,
+ 'type' => 14,
+ ));
+ }
+
+ return $list;
+ }
+
+ /**
+ * Updates a folder
+ */
+ public function updateFolder(Syncroton_Model_IFolder $folder)
+ {
+ if ($folder->serverId === $this->galFolder && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Updating GAL folder is not possible");
+ }
+
+ return parent::updateFolder($folder);
+ }
+
+ /**
+ * Deletes a folder
+ */
+ public function deleteFolder($folder)
+ {
+ if ($folder instanceof Syncroton_Model_IFolder) {
+ $folder = $folder->serverId;
+ }
+
+ if ($folder === $this->galFolder && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Deleting GAL folder is not possible");
+ }
+
+ return parent::deleteFolder($folder);
+ }
+
+ /**
+ * Empty folder (remove all entries and optionally subfolders)
+ *
+ * @param string $folderId Folder identifier
+ * @param array $options Options
+ */
+ public function emptyFolderContents($folderid, $options)
+ {
+ if ($folderid === $this->galFolder && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Emptying GAL folder is not possible");
+ }
+
+ return parent::emptyFolderContents($folderid, $options);
+ }
+
+ /**
+ * Moves object into another location (folder)
+ *
+ * @param string $srcFolderId Source folder identifier
+ * @param string $serverId Object identifier
+ * @param string $dstFolderId Destination folder identifier
+ *
+ * @throws Syncroton_Exception_Status
+ * @return string New object identifier
+ */
+ public function moveItem($srcFolderId, $serverId, $dstFolderId)
+ {
+ if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Moving GAL entries is not possible");
+ }
+
+ if ($srcFolderId === $this->galFolder && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Moving/Deleting GAL entries is not possible");
+ }
+
+ if ($dstFolderId === $this->galFolder && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Creating GAL entries is not possible");
+ }
+
+ return parent::moveItem($srcFolderId, $serverId, $dstFolderId);
+ }
+
+ /**
+ * Add entry
+ *
+ * @param string $folderId Folder identifier
+ * @param Syncroton_Model_IEntry $entry Entry object
+ *
+ * @return string ID of the created entry
+ */
+ public function createEntry($folderId, Syncroton_Model_IEntry $entry)
+ {
+ if ($folderId === $this->galFolder && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Creating GAL entries is not possible");
+ }
+
+ return parent::createEntry($folderId, $entry);
+ }
+
+ /**
+ * update existing entry
+ *
+ * @param string $folderId
+ * @param string $serverId
+ * @param SimpleXMLElement $entry
+ *
+ * @return string ID of the updated entry
+ */
+ public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
+ {
+ if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Updating GAL entries is not possible");
+ }
+
+ return parent::updateEntry($folderId, $serverId, $entry);
+ }
+
+ /**
+ * delete entry
+ *
+ * @param string $folderId
+ * @param string $serverId
+ * @param array $collectionData
+ */
+ public function deleteEntry($folderId, $serverId, $collectionData)
+ {
+ if (strpos($serverId, $this->galPrefix) === 0 && $this->hasGAL()) {
+ throw new Syncroton_Exception_AccessDenied("Deleting GAL entries is not possible");
+ }
+
+ return parent::deleteEntry($folderId, $serverId, $collectionData);
+ }
+
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
// specify object type, contact folders in Kolab might
// contain also ditribution-list objects, we'll skip them
return array(array('type', '=', $this->modelName));
}
+ /**
+ * Check if GAL synchronization is enabled for current device
+ */
+ protected function hasGAL()
+ {
+ return count($this->getGALSources());
+ }
+
+ /**
+ * Search for existing entries
+ *
+ * @param string $folderid Folder identifier
+ * @param array $filter Search filter
+ * @param int $result_type Type of the result (see RESULT_* constants)
+ *
+ * @return array|int Search result as count or array of uids/objects
+ */
+ protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID)
+ {
+ // GAL Folder exists, return result from LDAP only
+ if ($folderid === $this->galFolder && $this->hasGAL()) {
+ return $this->searchGALEntries($filter, $result_type);
+ }
+
+ $result = parent::searchEntries($folderid, $filter, $result_type);
+
+ // Merge results from LDAP
+ if ($this->hasGAL() && !$this->isMultiFolder()) {
+ $gal_result = $this->searchGALEntries($filter, $result_type);
+
+ if ($result_type == self::RESULT_COUNT) {
+ $result += $gal_result;
+ }
+ else {
+ $result = array_merge($result, $gal_result);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetches the entry from the backend
+ */
+ protected function getObject($folderid, $entryid, &$folder = null)
+ {
+ if (strpos($entryid, $this->galPrefix) === 0 && $this->hasGAL()) {
+ return $this->getGALEntry($entryid);
+ }
+
+ return parent::getObject($folderid, $entryid, $folder);
+ }
+
+ /**
+ * Search for existing LDAP entries
+ *
+ * @param array $filter Search filter
+ * @param int $result_type Type of the result (see RESULT_* constants)
+ *
+ * @return array|int Search result as count or array of uids/objects
+ */
+ protected function searchGALEntries($filter, $result_type)
+ {
+ // For GAL we don't check for changes.
+ // When something changed a new UID will be generated so the update
+ // will be done as delete + create
+ foreach ($filter as $f) {
+ if ($f[0] == 'changed') {
+ return $result_type == self::RESULT_COUNT ? 0 : array();
+ }
+ }
+
+ if ($this->galCache && ($result = $this->galCache->get('index')) !== null) {
+ $result = explode("\n", $result);
+ return $result_type == self::RESULT_COUNT ? count($result) : $result;
+ }
+
+ $result = array();
+
+ foreach ($this->getGALSources() as $source) {
+ if ($book = kolab_sync_data_gal::get_address_book($source['id'])) {
+ $book->reset();
+ $book->set_page(1);
+ $book->set_pagesize(10000);
+
+ $set = $book->list_records();
+ while ($contact = $set->next()) {
+ $result[] = $this->createGALEntryUID($contact, $source['id']);
+ }
+ }
+ }
+
+ if ($this->galCache) {
+ $this->galCache->set('index', implode("\n", $result));
+ }
+
+ return $result_type == self::RESULT_COUNT ? count($result) : $result;
+ }
+
+ /**
+ * Return specified LDAP entry
+ *
+ * @param string $serverId Entry identifier
+ *
+ * @return array Contact data
+ */
+ protected function getGALEntry($serverId)
+ {
+ list($source, $timestamp, $uid) = $this->resolveGALEntryUID($serverId);
+
+ if ($source && $uid && ($book = kolab_sync_data_gal::get_address_book($source))) {
+ $book->reset();
+
+ $set = $book->search('uid', array($uid), rcube_addressbook::SEARCH_STRICT, true, true);
+ $result = $set->first();
+
+ if ($result['uid'] == $uid && $result['changed'] == $timestamp) {
+ // As in kolab_sync_data_gal we use only one email address
+ if (empty($result['email'])) {
+ $emails = $book->get_col_values('email', $result, true);
+ $result['email'] = array($emails[0]);
+ }
+
+ return $result;
+ }
+ }
+ }
+
+ /**
+ * Return LDAP address books list
+ *
+ * @return array Address books array
+ */
+ protected function getGALSources()
+ {
+ if ($this->galSources === null) {
+ $rcube = rcube::get_instance();
+ $gal_sync = $rcube->config->get('activesync_gal_sync');
+ $enabled = false;
+
+ if ($gal_sync === true) {
+ $enabled = true;
+ }
+ else if (is_array($gal_sync)) {
+ $enabled = $this->deviceTypeFilter($gal_sync);
+ }
+
+ $this->galSources = $enabled ? kolab_sync_data_gal::get_address_sources() : array();
+
+ if ($cache_type = $rcube->config->get('activesync_gal_cache', 'db')) {
+ $cache_ttl = $rcube->config->get('activesync_gal_cache_ttl', '1d');
+ $this->galCache = $rcube->get_cache('activesync_gal', $cache_type, $cache_ttl, false);
+
+ // expunge cache every now and then
+ if (rand(0, 10) === 0) {
+ $this->galCache->expunge();
+ }
+ }
+ }
+
+ return $this->galSources;
+ }
+
+ /**
+ * Builds contact identifier from contact data and source id
+ */
+ protected function createGALEntryUID($contact, $source_id)
+ {
+ return $this->galPrefix . sprintf('%s:%s:%s', rcube_ldap::dn_encode($source_id), $contact['changed'], $contact['uid']);
+ }
+
+ /**
+ * Extracts contact identification data from contact identifier
+ */
+ protected function resolveGALEntryUID($uid)
+ {
+ if (strpos($uid, $this->galPrefix) === 0) {
+ $items = explode(':', substr($uid, strlen($this->galPrefix)));
+ $items[0] = rcube_ldap::dn_decode($items[0]);
+ return $items; // source, timestamp, uid
+ }
+
+ return array();
+ }
}
diff --git a/lib/kolab_sync_data_gal.php b/lib/kolab_sync_data_gal.php
index 297a4a9..95894e7 100644
--- a/lib/kolab_sync_data_gal.php
+++ b/lib/kolab_sync_data_gal.php
@@ -1,399 +1,390 @@
|
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak |
+--------------------------------------------------------------------------+
*/
/**
* GAL (Global Address List) data backend for Syncroton
*/
class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDataSearch
{
const MAX_SEARCH_RESULT = 100;
/**
* LDAP search result
*
* @var array
*/
protected $result = array();
/**
* LDAP address books list
*
* @var array
*/
- protected $address_books = array();
+ protected static $address_books = array();
/**
* Mapping from ActiveSync Contacts namespace fields
*/
protected $mapping = array(
'alias' => 'nickname',
'company' => 'organization',
'displayName' => 'name',
'emailAddress' => 'email',
'firstName' => 'firstname',
'lastName' => 'surname',
'mobilePhone' => 'phone.mobile',
'office' => 'office',
'picture' => 'photo',
'phone' => 'phone',
'title' => 'jobtitle',
);
/**
* Kolab object type
*
* @var string
*/
protected $modelName = 'contact';
/**
* Type of the default folder
*
* @var int
*/
protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT;
/**
* Default container for new entries
*
* @var string
*/
protected $defaultFolder = 'Contacts';
/**
* Type of user created folders
*
* @var int
*/
protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED;
/**
* the constructor
*
* @param Syncroton_Model_IDevice $device
* @param DateTime $syncTimeStamp
*/
public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp)
{
parent::__construct($device, $syncTimeStamp);
// Use configured fields mapping
$rcube = rcube::get_instance();
$fieldmap = (array) $rcube->config->get('activesync_gal_fieldmap');
if (!empty($fieldmap)) {
$fieldmap = array_intersec_key($fieldmap, array_keys($this->mapping));
$this->mapping = array_merge($this->mapping, $fieldmap);
}
}
/**
* Not used but required by parent class
*/
public function toKolab(Syncroton_Model_IEntry $data, $folderId, $entry = null)
{
}
/**
* Not used but required by parent class
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId)
{
}
/**
* Returns properties of a contact for Search response
*
* @param array $data Contact data
* @param array $options Search options
*
* @return Syncroton_Model_GAL Contact (GAL) object
*/
public function getSearchEntry($data, $options)
{
$result = array();
// Contacts namespace fields
foreach ($this->mapping as $key => $name) {
$value = $this->getLDAPDataItem($data, $name);
if (empty($value) || is_array($value)) {
continue;
}
switch ($name) {
case 'photo':
// @TODO: MaxPictures option
// ActiveSync limits photo size of GAL contact to 100KB
$maxsize = 102400;
if (!empty($options['picture']['maxSize'])) {
$maxsize = min($maxsize, $options['picture']['maxSize']);
}
if (strlen($value) > $maxsize) {
continue;
}
$value = new Syncroton_Model_GALPicture(array(
'data' => $value, // binary
'status' => Syncroton_Model_GALPicture::STATUS_SUCCESS,
));
break;
}
$result[$key] = $value;
}
return new Syncroton_Model_GAL($result);
}
/**
* ActiveSync Search handler
*
* @param Syncroton_Model_StoreRequest $store Search query parameters
*
* @return Syncroton_Model_StoreResponse Complete Search response
* @throws Exception
*/
public function search(Syncroton_Model_StoreRequest $store)
{
$options = $store->options;
$query = $store->query;
if (empty($query) || !is_string($query)) {
throw new Exception('Empty/invalid search request');
}
$records = array();
$rcube = rcube::get_instance();
// @TODO: caching with Options->RebuildResults support
- $books = $this->get_address_sources();
+ $books = self::get_address_sources();
$mode = 2; // use prefix mode
$fields = $rcube->config->get('contactlist_fields');
if (empty($fields)) {
$fields = '*';
}
foreach ($books as $idx => $book) {
- $book = $this->get_address_book($idx);
+ $book = self::get_address_book($idx);
if (!$book) {
continue;
}
$book->set_page(1);
$book->set_pagesize(self::MAX_SEARCH_RESULT);
$result = $book->search($fields, $query, $mode, true, true, 'email');
if (!$result->count) {
continue;
}
// get records
$result = $book->list_records();
while ($row = $result->next()) {
$row['sourceid'] = $idx;
// make sure 'email' item is there, convert all email:* into one
$row['email'] = $book->get_col_values('email', $row, true);
$key = $this->contact_key($row);
unset($row['_raw_attrib']); // save some memory, @TODO: do this in rcube_ldap
$records[$key] = $row;
}
// We don't want to search all sources if we've got already a lot of contacts
if (count($records) >= self::MAX_SEARCH_RESULT) {
break;
}
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
$records = array_values($records);
$response = new Syncroton_Model_StoreResponse();
// Calculate requested range
$start = (int) $options['range'][0];
$limit = (int) $options['range'][1] + 1;
$total = count($records);
$response->total = $total;
// Get requested chunk of data set
if ($total) {
if ($start > $total) {
$start = $total;
}
if ($limit > $total) {
$limit = max($start+1, $total);
}
if ($start > 0 || $limit < $total) {
$records = array_slice($records, $start, $limit-$start);
}
$response->range = array($start, $start + count($records) - 1);
}
// Build result array, convert to ActiveSync format
foreach ($records as $idx => $rec) {
$response->result[] = new Syncroton_Model_StoreResponseResult(array(
'longId' => $rec['ID'],
'properties' => $this->getSearchEntry($rec, $options),
));
unset($records[$idx]);
}
return $response;
}
/**
* Return instance of the internal address book class
*
* @param string $id Address book identifier
*
* @return rcube_contacts Address book object
*/
- protected function get_address_book($id)
+ public static function get_address_book($id)
{
$config = rcube::get_instance()->config;
$ldap_config = (array) $config->get('ldap_public');
// use existing instance
- if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
- $book = $this->address_books[$id];
+ if (isset(self::$address_books[$id]) && (self::$address_books[$id] instanceof rcube_addressbook)) {
+ $book = self::$address_books[$id];
}
else if ($id && $ldap_config[$id]) {
$book = new rcube_ldap($ldap_config[$id], $config->get('ldap_debug'),
$config->mail_domain($_SESSION['storage_host']));
}
if (!$book) {
rcube::raise_error(array(
'code' => 700, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Addressbook source ($id) not found!"),
true, false);
return null;
}
/*
// set configured sort order
if ($sort_col = $this->config->get('addressbook_sort_col'))
$book->set_sort_order($sort_col);
*/
// add to the 'books' array for shutdown function
- $this->address_books[$id] = $book;
+ self::$address_books[$id] = $book;
return $book;
}
/**
* Return LDAP address books list
*
* @return array Address books array
*/
- protected function get_address_sources()
+ public static function get_address_sources()
{
$config = rcube::get_instance()->config;
$ldap_config = (array) $config->get('ldap_public');
$async_books = $config->get('activesync_addressbooks');
if ($async_books === null) {
$async_books = (array) $config->get('autocomplete_addressbooks');
}
$list = array();
foreach ((array)$async_books as $id) {
$prop = $ldap_config[$id];
- // handle misconfiguration
- if (empty($prop) || !is_array($prop)) {
- continue;
+ if (!empty($prop) && is_array($prop)) {
+ $list[$id] = array(
+ 'id' => $id,
+ 'name' => $prop['name'],
+ );
}
-
- $list[$id] = array(
- 'id' => $id,
- 'name' => $prop['name'],
- );
-/*
- // register source for shutdown function
- if (!is_object($this->address_books[$id]))
- $this->address_books[$id] = $list[$id];
- }
-*/
}
return $list;
}
/**
* Creates contact key for sorting by
*/
protected function contact_key($row)
{
$key = $row['name'] . ':' . $row['sourceid'];
// add email to a key to not skip contacts with the same name
if (!empty($row['email'])) {
if (is_array($row['email'])) {
$key .= ':' . implode(':', $row['email']);
}
else {
$key .= ':' . $row['email'];
}
}
return $key;
}
/**
* Extracts data from Roundcube LDAP data array
*/
protected function getLDAPDataItem($data, $name)
{
list($name, $index) = explode(':', $name);
$name = str_replace('.', ':', $name);
if (isset($data[$name])) {
if ($index) {
return is_array($data[$name]) ? $data[$name][$index] : null;
}
return is_array($data[$name]) ? array_shift($data[$name]) : $data[$name];
}
return null;
}
}