Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1842039
kolab_sync_data.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
62 KB
Referenced Files
None
Subscribers
None
kolab_sync_data.php
View Options
<?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> |
+--------------------------------------------------------------------------+
*/
/**
* 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
();
/**
* Logger instance.
*
* @var kolab_sync_logger
*/
protected
$logger
;
/**
* 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
->
logger
=
Syncroton_Registry
::
get
(
Syncroton_Registry
::
LOGGERBACKEND
);
$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
()
{
$config
=
rcube
::
get_instance
()->
config
;
$blacklist
=
$config
->
get
(
'activesync_multifolder_blacklist_'
.
$this
->
modelName
);
if
(!
is_array
(
$blacklist
))
{
$blacklist
=
$config
->
get
(
'activesync_multifolder_blacklist'
);
}
if
(
is_array
(
$blacklist
))
{
return
!
$this
->
deviceTypeFilter
(
$blacklist
);
}
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
;
$parent
=
null
;
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
);
if
(!
is_array
(
$list
))
{
throw
new
Syncroton_Exception_Status_ItemOperations
(
Syncroton_Exception_Status_ItemOperations
::
ITEM_SERVER_ERROR
);
}
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
[
'_serverId'
];
}
/**
* 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
(
'entry 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
[
'_serverId'
];
}
/**
* 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
if
(!
empty
(
$uids
))
{
$result
=
array_merge
(
$result
,
$this
->
applyServerId
(
$uids
,
$folder
));
}
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
)
&&
!
empty
(
$uids
))
{
$result
=
array_unique
(
array_merge
(
$result
,
$this
->
applyServerId
(
$uids
,
$folder
)));
}
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
(
isset
(
$this
->
tag_categories
)
&&
!
$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
"
,
(
array
)
$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
,
$syncState
->
counter
);
$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
{
if
(
$this
->
getChangedEntriesCount
(
$folder
->
serverId
,
$syncState
->
lastsync
,
null
,
$folder
->
lastfiltertype
))
{
return
true
;
}
$allClientEntries
=
$contentBackend
->
getFolderState
(
$this
->
device
,
$folder
,
$syncState
->
counter
);
// @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
);
if
(
empty
(
$folders
))
{
return
null
;
}
foreach
(
$folders
as
$folderid
)
{
$foldername
=
$this
->
backend
->
folder_id2name
(
$folderid
,
$this
->
device
->
deviceid
);
$folder
=
$this
->
getFolderObject
(
$foldername
);
if
(
$folder
&&
$folder
->
valid
)
{
$crc
=
null
;
$uid
=
$entryid
;
// See self::serverId() for full explanation
// Use (slower) UID prefix matching...
if
(
preg_match
(
'/^CRC([0-9A-Fa-f]{8})(.+)$/'
,
$uid
,
$matches
))
{
$crc
=
$matches
[
1
];
$uid
=
$matches
[
2
];
if
(
strlen
(
$entryid
)
>=
64
)
{
foreach
(
$folder
->
select
(
array
(
array
(
'uid'
,
'~*'
,
$uid
)))
as
$object
)
{
if
((
$object
[
'uid'
]
==
$uid
||
strpos
(
$object
[
'uid'
],
$uid
)
===
0
)
&&
$crc
==
$this
->
objectCRC
(
$object
[
'uid'
],
$folder
)
)
{
$object
[
'_folderid'
]
=
$folderid
;
return
$object
;
}
}
continue
;
}
}
// Or (faster) strict UID matching...
if
((
$object
=
$folder
->
get_object
(
$uid
))
&&
(
$crc
===
null
||
$crc
==
$this
->
objectCRC
(
$object
[
'uid'
],
$folder
))
)
{
$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
(!
empty
(
$data
[
'categories'
])
&&
isset
(
$this
->
tag_categories
)
&&
$this
->
tag_categories
)
{
$tags
=
$data
[
'categories'
];
unset
(
$data
[
'categories'
]);
}
$foldername
=
$this
->
backend
->
folder_id2name
(
$folderid
,
$this
->
device
->
deviceid
);
$folder
=
$this
->
getFolderObject
(
$foldername
);
// 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
))
{
$this
->
setKolabTags
(
$data
[
'uid'
],
$tags
);
}
$data
[
'_serverId'
]
=
$this
->
serverId
(
$data
[
'uid'
],
$folder
);
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
(
isset
(
$this
->
tag_categories
)
&&
$this
->
tag_categories
&&
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
))
{
if
(
isset
(
$tags
))
{
$this
->
setKolabTags
(
$data
[
'uid'
],
$tags
);
}
$data
[
'_serverId'
]
=
$this
->
serverId
(
$object
[
'uid'
],
$folder
);
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
(
$object
[
'uid'
]))
{
if
(
isset
(
$this
->
tag_categories
)
&&
$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
||
!
is_array
(
$this
->
imap_folders
))
{
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
);
}
/**
* Returns folder ID from Kolab folder object
*/
protected
function
getFolderId
(
$folder
)
{
if
(!
$this
->
isMultiFolder
())
{
return
$this
->
defaultRootFolder
;
}
return
$this
->
backend
->
folder_id
(
$folder
->
get_name
(),
$folder
->
get_type
());
}
/**
* 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
;
}
// custom properties
if
(
$count
==
2
&&
$name_items
[
0
]
==
'x-custom'
)
{
$value
=
null
;
foreach
((
array
)
$data
[
'x-custom'
]
as
$val
)
{
if
(
is_array
(
$val
)
&&
$val
[
0
]
==
$name_items
[
1
])
{
$value
=
$val
[
1
];
break
;
}
}
return
$value
;
}
$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
);
$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
(!
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
;
}
// custom properties
if
(
$count
==
2
&&
$name_items
[
0
]
==
'x-custom'
)
{
$data
[
'x-custom'
]
=
isset
(
$data
[
'x-custom'
])
?
((
array
)
$data
[
'x-custom'
])
:
array
();
foreach
(
$data
[
'x-custom'
]
as
$idx
=>
$val
)
{
if
(
is_array
(
$val
)
&&
$val
[
0
]
==
$name_items
[
1
])
{
$data
[
'x-custom'
][
$idx
][
1
]
=
$value
;
return
;
}
}
$data
[
'x-custom'
][]
=
array
(
$name_items
[
1
],
$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
);
$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
(!
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
;
}
// custom properties
if
(
$count
==
2
&&
$name_items
[
0
]
==
'x-custom'
)
{
foreach
((
array
)
$data
[
'x-custom'
]
as
$idx
=>
$val
)
{
if
(
is_array
(
$val
)
&&
$val
[
0
]
==
$name_items
[
1
])
{
unset
(
$data
[
'x-custom'
][
$idx
]);
}
}
}
$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|null Body value
*/
protected
function
getBody
(
$body
,
$type
=
null
)
{
$data
=
null
;
if
(
$body
&&
$body
->
data
)
{
$data
=
$body
->
data
;
}
if
(!
$data
||
empty
(
$type
))
{
return
null
;
}
$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 <html> or <body> 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
(
isset
(
$prefs
[
$type
][
'truncationSize'
])
&&
(
$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'
])
||
!
empty
(
$data
[
'recurrence_date'
]))
{
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
;
default
:
return
;
}
// Skip all empty values (T2519)
if
(
$recurrence
[
'type'
]
!=
self
::
RECUR_TYPE_DAILY
)
{
$recurrence
=
array_filter
(
$recurrence
);
}
// required field
$recurrence
[
'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
)
&&
!(
$data
->
recurrence
instanceof
Syncroton_Model_TaskRecurrence
)
)
{
return
;
}
if
(!
isset
(
$data
->
recurrence
->
type
))
{
return
;
}
$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)
if
(!
empty
(
$data
[
'recurrence'
][
'EXCEPTIONS'
]))
{
foreach
((
array
)
$data
[
'recurrence'
][
'EXCEPTIONS'
]
as
$exception
)
{
$exception
[
'_mailbox'
]
=
$data
[
'_mailbox'
];
$ex
=
$this
->
getEntry
(
$collection
,
$exception
,
true
);
$date
=
clone
(
$exception
[
'recurrence_date'
]
?:
$ex
[
'startTime'
]);
$ex
[
'exceptionStartTime'
]
=
self
::
set_exception_time
(
$date
,
$data
[
'_start'
]);
// 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)
if
(!
empty
(
$data
[
'recurrence'
][
'EXDATE'
]))
{
foreach
((
array
)
$data
[
'recurrence'
][
'EXDATE'
]
as
$exception
)
{
if
(!(
$exception
instanceof
DateTime
))
{
continue
;
}
$ex
=
array
(
'deleted'
=>
1
,
'exceptionStartTime'
=>
self
::
set_exception_time
(
$exception
,
$data
[
'_start'
]),
);
$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
)
{
$date
=
clone
$exception
->
exceptionStartTime
;
if
(
$timezone
)
{
$date
->
setTimezone
(
$timezone
);
}
if
(
$exception
->
deleted
)
{
$date
->
setTime
(
0
,
0
,
0
);
$rrule
[
'EXDATE'
][]
=
$date
;
}
else
{
$ex
=
$this
->
toKolab
(
$exception
,
$folderid
,
null
,
$timezone
);
$ex
[
'recurrence_date'
]
=
$date
;
if
(
$data
->
allDayEvent
)
{
$ex
[
'allday'
]
=
1
;
}
$rrule
[
'EXCEPTIONS'
][]
=
$ex
;
}
}
}
if
(
empty
(
$rrule
[
'EXDATE'
]))
{
unset
(
$rrule
[
'EXDATE'
]);
}
if
(
empty
(
$rrule
[
'EXCEPTIONS'
]))
{
unset
(
$rrule
[
'EXCEPTIONS'
]);
}
}
/**
* Sets ExceptionStartTime according to occurrence date and event start time
*/
protected
static
function
set_exception_time
(
$exception_date
,
$event_start
)
{
if
(
$exception_date
&&
$event_start
)
{
$hour
=
$event_start
->
format
(
'H'
);
$minute
=
$event_start
->
format
(
'i'
);
$second
=
$event_start
->
format
(
's'
);
$exception_date
->
setTime
(
$hour
,
$minute
,
$second
);
$exception_date
->
_dateonly
=
false
;
return
self
::
date_from_kolab
(
$exception_date
);
}
}
/**
* 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
;
}
/**
* Returns all email addresses of the current user
*/
protected
function
user_emails
()
{
$user_emails
=
kolab_sync
::
get_instance
()->
user
->
list_emails
();
$user_emails
=
array_map
(
function
(
$v
)
{
return
$v
[
'email'
];
},
$user_emails
);
return
$user_emails
;
}
/**
* Generate CRC-based ServerId from object UID
*/
protected
function
serverId
(
$uid
,
$folder
)
{
if
(
$this
->
modelName
==
'mail'
)
{
return
$uid
;
}
// When ActiveSync communicates with the client, it refers to objects with a ServerId
// We can't use object UID for ServerId because:
// - ServerId is limited to 64 chars,
// - there can be multiple calendars with a copy of the same event.
//
// The solution is to; Take the original UID, and regardless of its length, execute the following:
// - Hash the UID concatenated with the Folder ID using CRC32b,
// - Prefix the UID with 'CRC' and the hash string,
// - Tryncate the result to 64 characters.
//
// Searching for the server-side copy of the object now follows the logic;
// - If the ServerId is prefixed with 'CRC', strip off the first 11 characters
// and we search for the UID using the remainder;
// - if the UID is shorter than 53 characters, it'll be the complete UID,
// - if the UID is longer than 53 characters, it'll be the truncated UID,
// and we search for a wildcard match of <uid>*
// When multiple copies of the same event are found, the same CRC32b hash can be used
// on the events metadata (i.e. the copy's UID and Folder ID), and compared with the CRC from the ServerId.
// ServerId is max. 64 characters, below we generate a string of max. 64 chars
// Note: crc32b is always 8 characters
return
'CRC'
.
$this
->
objectCRC
(
$uid
,
$folder
)
.
substr
(
$uid
,
0
,
53
);
}
/**
* Calculate checksum on object UID and folder UID
*/
protected
function
objectCRC
(
$uid
,
$folder
)
{
if
(!
is_object
(
$folder
))
{
$folder
=
$this
->
getFolderObject
(
$folder
);
}
$folder_uid
=
$folder
->
get_uid
();
return
strtoupper
(
hash
(
'crc32b'
,
$folder_uid
.
$uid
));
// always 8 chars
}
/**
* Apply serverId() on a set of uids
*/
protected
function
applyServerId
(
$uids
,
$folder
)
{
if
(!
empty
(
$uids
)
&&
$this
->
modelName
!=
'mail'
)
{
$self
=
$this
;
$func
=
function
(
$uid
)
use
(
$self
,
$folder
)
{
return
$self
->
serverId
(
$uid
,
$folder
);
};
$uids
=
array_map
(
$func
,
$uids
);
}
return
$uids
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Mon, Aug 25, 7:20 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
253946
Default Alt Text
kolab_sync_data.php (62 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment