Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117882943
kolab_sync_data.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
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
;
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
(!
$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
);
$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
);
// @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
(
$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
(
$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
(
$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'
)
{
foreach
((
array
)
$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 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 <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
((
$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)
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)
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
)
{
if
(
$exception
->
deleted
)
{
$date
=
clone
$exception
->
exceptionStartTime
;
if
(
$timezone
)
{
$date
->
setTimezone
(
$timezone
);
}
$date
->
setTime
(
0
,
0
,
0
);
$rrule
[
'EXDATE'
][]
=
$date
;
}
else
{
$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'
]);
}
}
/**
* 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, Apr 6, 12:48 AM (5 d, 7 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
04/97/09174b58ec95db55d31bfca66e62
Default Alt Text
kolab_sync_data.php (62 KB)
Attached To
Mode
rS syncroton
Attached
Detach File
Event Timeline