Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117880745
kolab_sync_data_calendar.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
39 KB
Referenced Files
None
Subscribers
None
kolab_sync_data_calendar.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> |
+--------------------------------------------------------------------------+
*/
/**
* Calendar (Events) data class for Syncroton
*/
class
kolab_sync_data_calendar
extends
kolab_sync_data
implements
Syncroton_Data_IDataCalendar
{
/**
* Mapping from ActiveSync Calendar namespace fields
*/
protected
$mapping
=
array
(
'allDayEvent'
=>
'allday'
,
'startTime'
=>
'start'
,
// keep it before endTime here
//'attendees' => 'attendees',
'body'
=>
'description'
,
//'bodyTruncated' => 'bodytruncated',
'busyStatus'
=>
'free_busy'
,
//'categories' => 'categories',
'dtStamp'
=>
'changed'
,
'endTime'
=>
'end'
,
//'exceptions' => 'exceptions',
'location'
=>
'location'
,
//'meetingStatus' => 'meetingstatus',
//'organizerEmail' => 'organizeremail',
//'organizerName' => 'organizername',
//'recurrence' => 'recurrence',
//'reminder' => 'reminder',
//'responseRequested' => 'responserequested',
//'responseType' => 'responsetype',
'sensitivity'
=>
'sensitivity'
,
'subject'
=>
'title'
,
//'timezone' => 'timezone',
'uID'
=>
'uid'
,
);
/**
* Kolab object type
*
* @var string
*/
protected
$modelName
=
'event'
;
/**
* Type of the default folder
*
* @var int
*/
protected
$defaultFolderType
=
Syncroton_Command_FolderSync
::
FOLDERTYPE_CALENDAR
;
/**
* Default container for new entries
*
* @var string
*/
protected
$defaultFolder
=
'Calendar'
;
/**
* Type of user created folders
*
* @var int
*/
protected
$folderType
=
Syncroton_Command_FolderSync
::
FOLDERTYPE_CALENDAR_USER_CREATED
;
/**
* attendee status
*/
const
ATTENDEE_STATUS_UNKNOWN
=
0
;
const
ATTENDEE_STATUS_TENTATIVE
=
2
;
const
ATTENDEE_STATUS_ACCEPTED
=
3
;
const
ATTENDEE_STATUS_DECLINED
=
4
;
const
ATTENDEE_STATUS_NOTRESPONDED
=
5
;
/**
* attendee types
*/
const
ATTENDEE_TYPE_REQUIRED
=
1
;
const
ATTENDEE_TYPE_OPTIONAL
=
2
;
const
ATTENDEE_TYPE_RESOURCE
=
3
;
/**
* busy status constants
*/
const
BUSY_STATUS_FREE
=
0
;
const
BUSY_STATUS_TENTATIVE
=
1
;
const
BUSY_STATUS_BUSY
=
2
;
const
BUSY_STATUS_OUTOFOFFICE
=
3
;
/**
* Sensitivity values
*/
const
SENSITIVITY_NORMAL
=
0
;
const
SENSITIVITY_PERSONAL
=
1
;
const
SENSITIVITY_PRIVATE
=
2
;
const
SENSITIVITY_CONFIDENTIAL
=
3
;
const
KEY_DTSTAMP
=
'x-custom.X-ACTIVESYNC-DTSTAMP'
;
const
KEY_RESPONSE_DTSTAMP
=
'x-custom.X-ACTIVESYNC-RESPONSE-DTSTAMP'
;
/**
* Mapping of attendee status
*
* @var array
*/
protected
$attendeeStatusMap
=
array
(
'UNKNOWN'
=>
self
::
ATTENDEE_STATUS_UNKNOWN
,
'TENTATIVE'
=>
self
::
ATTENDEE_STATUS_TENTATIVE
,
'ACCEPTED'
=>
self
::
ATTENDEE_STATUS_ACCEPTED
,
'DECLINED'
=>
self
::
ATTENDEE_STATUS_DECLINED
,
'DELEGATED'
=>
self
::
ATTENDEE_STATUS_UNKNOWN
,
'NEEDS-ACTION'
=>
self
::
ATTENDEE_STATUS_NOTRESPONDED
,
);
/**
* Mapping of attendee type
*
* NOTE: recurrences need extra handling!
* @var array
*/
protected
$attendeeTypeMap
=
array
(
'REQ-PARTICIPANT'
=>
self
::
ATTENDEE_TYPE_REQUIRED
,
'OPT-PARTICIPANT'
=>
self
::
ATTENDEE_TYPE_OPTIONAL
,
// 'NON-PARTICIPANT' => self::ATTENDEE_TYPE_RESOURCE,
// 'CHAIR' => self::ATTENDEE_TYPE_RESOURCE,
);
/**
* Mapping of busy status
*
* @var array
*/
protected
$busyStatusMap
=
array
(
'free'
=>
self
::
BUSY_STATUS_FREE
,
'tentative'
=>
self
::
BUSY_STATUS_TENTATIVE
,
'busy'
=>
self
::
BUSY_STATUS_BUSY
,
'outofoffice'
=>
self
::
BUSY_STATUS_OUTOFOFFICE
,
);
/**
* mapping of sensitivity
*
* @var array
*/
protected
$sensitivityMap
=
array
(
'public'
=>
self
::
SENSITIVITY_PERSONAL
,
'private'
=>
self
::
SENSITIVITY_PRIVATE
,
'confidential'
=>
self
::
SENSITIVITY_CONFIDENTIAL
,
);
/**
* Appends contact data to xml element
*
* @param Syncroton_Model_SyncCollection $collection Collection data
* @param string $serverId Local entry identifier
* @param boolean $as_array Return entry as array
*
* @return array|Syncroton_Model_Event|array Event object
*/
public
function
getEntry
(
Syncroton_Model_SyncCollection
$collection
,
$serverId
,
$as_array
=
false
)
{
$event
=
is_array
(
$serverId
)
?
$serverId
:
$this
->
getObject
(
$collection
->
collectionId
,
$serverId
);
$config
=
$this
->
getFolderConfig
(
$event
[
'_mailbox'
]);
$result
=
array
();
// Timezone
// Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows
// only one timezone per-event. We'll use timezone of the start date
if
(
$event
[
'start'
]
instanceof
DateTime
)
{
$timezone
=
$event
[
'start'
]->
getTimezone
();
if
(
$timezone
&&
(
$tz_name
=
$timezone
->
getName
())
!=
'UTC'
)
{
$tzc
=
kolab_sync_timezone_converter
::
getInstance
();
if
(
$tz_name
=
$tzc
->
encodeTimezone
(
$tz_name
))
{
$result
[
'timezone'
]
=
$tz_name
;
}
}
}
// Calendar namespace fields
foreach
(
$this
->
mapping
as
$key
=>
$name
)
{
$value
=
$this
->
getKolabDataItem
(
$event
,
$name
);
switch
(
$name
)
{
case
'changed'
:
case
'end'
:
case
'start'
:
// For all-day events Kolab uses different times
// At least Android doesn't display such event as all-day event
if
(
$value
&&
is_a
(
$value
,
'DateTime'
))
{
$date
=
clone
$value
;
if
(
$event
[
'allday'
])
{
// need this for self::date_from_kolab()
$date
->
_dateonly
=
false
;
if
(
$name
==
'start'
)
{
$date
->
setTime
(
0
,
0
,
0
);
}
else
if
(
$name
==
'end'
)
{
$date
->
setTime
(
0
,
0
,
0
);
$date
->
modify
(
'+1 day'
);
}
}
// set this date for use in recurrence exceptions handling
if
(
$name
==
'start'
)
{
$event
[
'_start'
]
=
$date
;
}
$value
=
self
::
date_from_kolab
(
$date
);
}
break
;
case
'sensitivity'
:
$value
=
intval
(
$this
->
sensitivityMap
[
$value
]);
break
;
case
'free_busy'
:
$value
=
$this
->
busyStatusMap
[
$value
];
break
;
case
'description'
:
$value
=
$this
->
body_from_kolab
(
$value
,
$collection
);
break
;
}
// Ignore empty values (but not integer 0)
if
((
empty
(
$value
)
||
is_array
(
$value
))
&&
$value
!==
0
)
{
continue
;
}
$result
[
$key
]
=
$value
;
}
// Event reminder time
if
(
$config
[
'ALARMS'
])
{
$result
[
'reminder'
]
=
$this
->
from_kolab_alarm
(
$event
);
}
$result
[
'categories'
]
=
array
();
$result
[
'attendees'
]
=
array
();
// Categories, Roundcube Calendar plugin supports only one category at a time
if
(!
empty
(
$event
[
'categories'
]))
{
$result
[
'categories'
]
=
(
array
)
$event
[
'categories'
];
}
// Organizer
if
(!
empty
(
$event
[
'attendees'
]))
{
foreach
(
$event
[
'attendees'
]
as
$idx
=>
$attendee
)
{
if
(
$attendee
[
'role'
]
==
'ORGANIZER'
)
{
if
(
$name
=
$attendee
[
'name'
])
{
$result
[
'organizerName'
]
=
$name
;
}
if
(
$email
=
$attendee
[
'email'
])
{
$result
[
'organizerEmail'
]
=
$email
;
}
unset
(
$event
[
'attendees'
][
$idx
]);
break
;
}
}
}
// Attendees
if
(!
empty
(
$event
[
'attendees'
]))
{
$user_emails
=
$this
->
user_emails
();
$user_rsvp
=
false
;
foreach
(
$event
[
'attendees'
]
as
$idx
=>
$attendee
)
{
$att
=
array
();
if
(
$email
=
$attendee
[
'email'
])
{
$att
[
'email'
]
=
$email
;
}
else
{
// In Activesync email is required
continue
;
}
$att
[
'name'
]
=
$attendee
[
'name'
]
?:
$email
;
$type
=
isset
(
$attendee
[
'role'
])
?
$this
->
attendeeTypeMap
[
$attendee
[
'role'
]]
:
null
;
$status
=
isset
(
$attendee
[
'status'
])
?
$this
->
attendeeStatusMap
[
$attendee
[
'status'
]]
:
null
;
if
(
$this
->
asversion
>=
12
)
{
$att
[
'attendeeType'
]
=
$type
?:
self
::
ATTENDEE_TYPE_REQUIRED
;
$att
[
'attendeeStatus'
]
=
$status
?:
self
::
ATTENDEE_STATUS_UNKNOWN
;
}
if
(
$email
&&
in_array_nocase
(
$email
,
$user_emails
))
{
$user_rsvp
=
!
empty
(
$attendee
[
'rsvp'
]);
$resp_type
=
$status
?:
self
::
ATTENDEE_STATUS_UNKNOWN
;
}
$result
[
'attendees'
][]
=
new
Syncroton_Model_EventAttendee
(
$att
);
}
}
// Event meeting status
$this
->
meeting_status_from_kolab
(
$collection
,
$event
,
$result
);
// Recurrence (and exceptions)
$this
->
recurrence_from_kolab
(
$collection
,
$event
,
$result
);
// RSVP status
$result
[
'responseRequested'
]
=
$result
[
'meetingStatus'
]
==
3
&&
$user_rsvp
?
1
:
0
;
$result
[
'responseType'
]
=
$result
[
'meetingStatus'
]
==
3
?
$resp_type
:
null
;
return
$as_array
?
$result
:
new
Syncroton_Model_Event
(
$result
);
}
/**
* convert contact from xml to libkolab array
*
* @param Syncroton_Model_IEntry $data Contact to convert
* @param string $folderid Folder identifier
* @param array $entry Existing entry
* @param DateTimeZone $timezone Timezone of the event
*
* @return array
*/
public
function
toKolab
(
Syncroton_Model_IEntry
$data
,
$folderid
,
$entry
=
null
,
$timezone
=
null
)
{
$event
=
!
empty
(
$entry
)
?
$entry
:
array
();
$foldername
=
isset
(
$event
[
'_mailbox'
])
?
$event
[
'_mailbox'
]
:
$this
->
getFolderName
(
$folderid
);
$config
=
$this
->
getFolderConfig
(
$foldername
);
$is_exception
=
$data
instanceof
Syncroton_Model_EventException
;
$dummy_tz
=
str_repeat
(
'A'
,
230
)
.
'=='
;
$is_outlook
=
stripos
(
$this
->
device
->
devicetype
,
'outlook'
)
!==
false
;
// check data validity
$this
->
check_event
(
$data
);
if
(!
empty
(
$event
[
'start'
])
&&
(
$event
[
'start'
]
instanceof
DateTime
))
{
$old_timezone
=
$event
[
'start'
]->
getTimezone
();
}
// Timezone
if
(!
$timezone
&&
isset
(
$data
->
timezone
)
&&
$data
->
timezone
!=
$dummy_tz
)
{
$tzc
=
kolab_sync_timezone_converter
::
getInstance
();
$expected
=
$old_timezone
?:
kolab_format
::
$timezone
;
try
{
$timezone
=
$tzc
->
getTimezone
(
$data
->
timezone
,
$expected
->
getName
());
$timezone
=
new
DateTimeZone
(
$timezone
);
}
catch
(
Exception
$e
)
{
$timezone
=
null
;
}
}
if
(
empty
(
$timezone
))
{
$timezone
=
$old_timezone
?:
new
DateTimeZone
(
'UTC'
);
}
$event
[
'allday'
]
=
0
;
// Calendar namespace fields
foreach
(
$this
->
mapping
as
$key
=>
$name
)
{
// skip UID field, unsupported in event exceptions
// we need to do this here, because the next line (data getter) will throw an exception
if
(
$is_exception
&&
$key
==
'uID'
)
{
continue
;
}
$value
=
$data
->
$key
;
switch
(
$name
)
{
case
'changed'
:
$value
=
null
;
break
;
case
'end'
:
case
'start'
:
if
(
$timezone
&&
$value
)
{
$value
->
setTimezone
(
$timezone
);
}
if
(
$value
&&
$data
->
allDayEvent
)
{
$value
->
_dateonly
=
true
;
// In ActiveSync all-day event ends on 00:00:00 next day
// In Kolab we just ignore the time spec.
if
(
$name
==
'end'
)
{
$diff
=
date_diff
(
$event
[
'start'
],
$value
);
$value
=
clone
$event
[
'start'
];
if
(
$diff
->
days
>
1
)
{
$value
->
add
(
new
DateInterval
(
'P'
.
(
$diff
->
days
-
1
)
.
'D'
));
}
}
}
break
;
case
'sensitivity'
:
$map
=
array_flip
(
$this
->
sensitivityMap
);
$value
=
$map
[
$value
];
break
;
case
'free_busy'
:
$map
=
array_flip
(
$this
->
busyStatusMap
);
$value
=
$map
[
$value
];
break
;
case
'description'
:
$value
=
$this
->
getBody
(
$value
,
Syncroton_Model_EmailBody
::
TYPE_PLAINTEXT
);
// If description isn't specified keep old description
if
(
$value
===
null
)
{
continue
2
;
}
break
;
}
$this
->
setKolabDataItem
(
$event
,
$name
,
$value
);
}
// Try to fix allday events from Android
// It doesn't set all-day flag but the period is a whole day
if
(!
$event
[
'allday'
]
&&
$event
[
'end'
]
&&
$event
[
'start'
])
{
$interval
=
@
date_diff
(
$event
[
'start'
],
$event
[
'end'
]);
if
(
$interval
&&
$interval
->
format
(
'%y%m%d%h%i%s'
)
===
'001000'
)
{
$event
[
'allday'
]
=
1
;
$event
[
'end'
]
=
clone
$event
[
'start'
];
}
}
// Reminder
// @TODO: should alarms be used when importing event from phone?
if
(
$config
[
'ALARMS'
])
{
$event
[
'valarms'
]
=
$this
->
to_kolab_alarm
(
$data
->
reminder
,
$event
);
}
$attendees
=
array
();
$categories
=
array
();
// Categories
if
(
isset
(
$data
->
categories
))
{
foreach
(
$data
->
categories
as
$category
)
{
$categories
[]
=
$category
;
}
}
// Organizer
if
(!
$is_exception
&&
(
$organizer_email
=
$data
->
organizerEmail
))
{
$attendees
[]
=
array
(
'role'
=>
'ORGANIZER'
,
'name'
=>
$data
->
organizerName
,
'email'
=>
$organizer_email
,
);
}
// Attendees
// Outlook 2013 sends a dummy update just after MeetingResponse has been processed,
// this update resets attendee status set in the MeetingResponse request.
// We ignore changes to attendees data on such updates
if
(
$is_outlook
&&
$this
->
isDummyOutlookUpdate
(
$data
,
$entry
,
$event
))
{
$attendees
=
$entry
[
'attendees'
];
}
else
if
(
isset
(
$data
->
attendees
))
{
$statusMap
=
array_flip
(
$this
->
attendeeStatusMap
);
foreach
(
$data
->
attendees
as
$attendee
)
{
if
(
$attendee
->
email
&&
$attendee
->
email
==
$organizer_email
)
{
continue
;
}
$role
=
false
;
if
(
isset
(
$attendee
->
attendeeType
))
{
$role
=
array_search
(
$attendee
->
attendeeType
,
$this
->
attendeeTypeMap
);
}
if
(
$role
===
false
)
{
$role
=
array_search
(
self
::
ATTENDEE_TYPE_REQUIRED
,
$this
->
attendeeTypeMap
);
}
$_attendee
=
array
(
'role'
=>
$role
,
'name'
=>
$attendee
->
name
!=
$attendee
->
email
?
$attendee
->
name
:
''
,
'email'
=>
$attendee
->
email
,
);
if
(
isset
(
$attendee
->
attendeeStatus
))
{
$_attendee
[
'status'
]
=
$attendee
->
attendeeStatus
?
array_search
(
$attendee
->
attendeeStatus
,
$this
->
attendeeStatusMap
)
:
null
;
if
(!
$_attendee
[
'status'
])
{
$_attendee
[
'status'
]
=
'NEEDS-ACTION'
;
$_attendee
[
'rsvp'
]
=
true
;
}
}
else
if
(!
empty
(
$event
[
'attendees'
])
&&
!
empty
(
$attendee
->
email
))
{
// copy the old attendee status
foreach
(
$event
[
'attendees'
]
as
$old_attendee
)
{
if
(
$old_attendee
[
'email'
]
==
$_attendee
[
'email'
]
&&
isset
(
$old_attendee
[
'status'
]))
{
$_attendee
[
'status'
]
=
$old_attendee
[
'status'
];
$_attendee
[
'rsvp'
]
=
$old_attendee
[
'rsvp'
];
break
;
}
}
}
$attendees
[]
=
$_attendee
;
}
}
// Make sure the event has the organizer set
if
(!
$organizer_email
&&
(
$identity
=
kolab_sync
::
get_instance
()->
user
->
get_identity
()))
{
$attendees
[]
=
array
(
'role'
=>
'ORGANIZER'
,
'name'
=>
$identity
[
'name'
],
'email'
=>
$identity
[
'email'
],
);
}
$event
[
'attendees'
]
=
$attendees
;
$event
[
'categories'
]
=
$categories
;
// recurrence (and exceptions)
if
(!
$is_exception
)
{
$event
[
'recurrence'
]
=
$this
->
recurrence_to_kolab
(
$data
,
$folderid
,
$timezone
);
}
// Bump SEQUENCE number on update (Outlook only).
// It's been confirmed that any change of the event that has attendees specified
// bumps SEQUENCE number of the event (we can see this in sent iTips).
// Unfortunately Outlook also sends an update when no SEQUENCE bump
// is needed, e.g. when updating attendee status.
// We try our best to bump the SEQUENCE only when expected
if
(!
empty
(
$entry
)
&&
!
$is_exception
&&
!
empty
(
$data
->
attendees
)
&&
$data
->
timezone
!=
$dummy_tz
)
{
if
(
$last_update
=
$this
->
getKolabDataItem
(
$event
,
self
::
KEY_DTSTAMP
))
{
$last_update
=
new
DateTime
(
$last_update
);
}
if
(
$data
->
dtStamp
&&
$data
->
dtStamp
!=
$last_update
)
{
if
(
$this
->
has_significant_changes
(
$event
,
$entry
))
{
$event
[
'sequence'
]++;
$this
->
logger
->
debug
(
'Found significant changes in the updated event. Bumping SEQUENCE to '
.
$event
[
'sequence'
]);
}
}
}
// Because we use last event modification time above, we make sure
// the event modification time is not (re)set by the server,
// we use the original Outlook's timestamp.
if
(
$is_outlook
&&
$data
->
dtStamp
)
{
$this
->
setKolabDataItem
(
$event
,
self
::
KEY_DTSTAMP
,
$data
->
dtStamp
->
format
(
DateTime
::
ATOM
));
}
// This prevents kolab_format code to bump the sequence when not needed
if
(!
isset
(
$event
[
'sequence'
]))
{
$event
[
'sequence'
]
=
0
;
}
return
$event
;
}
/**
* Set attendee status for meeting
*
* @param Syncroton_Model_MeetingResponse $request The meeting response
*
* @return string ID of new calendar entry
*/
public
function
setAttendeeStatus
(
Syncroton_Model_MeetingResponse
$request
)
{
$status_map
=
array
(
1
=>
'ACCEPTED'
,
2
=>
'TENTATIVE'
,
3
=>
'DECLINED'
,
);
if
(
$status
=
$status_map
[
$request
->
userResponse
])
{
// extract event from the invitation
list
(
$event
,
$existing
)
=
$this
->
get_event_from_invitation
(
$request
);
/*
switch ($status) {
case 'ACCEPTED': $event['free_busy'] = 'busy'; break;
case 'TENTATIVE': $event['free_busy'] = 'tentative'; break;
case 'DECLINED': $event['free_busy'] = 'free'; break;
}
*/
// Store Outlook response timestamp for further use
if
(
stripos
(
$this
->
device
->
devicetype
,
'outlook'
)
!==
false
)
{
$dtstamp
=
new
DateTime
(
'now'
,
new
DateTimeZone
(
'UTC'
));
$dtstamp
=
$dtstamp
->
format
(
DateTime
::
ATOM
);
}
// Update/Save the event
if
(
empty
(
$existing
))
{
if
(
$dtstamp
)
{
$this
->
setKolabDataItem
(
$event
,
self
::
KEY_RESPONSE_DTSTAMP
,
$dtstamp
);
}
$folder
=
$this
->
save_event
(
$event
,
$status
);
// Create SyncState for the new event, so it is not synced twice
if
(
$folder
)
{
$folderId
=
$this
->
getFolderId
(
$folder
);
try
{
$syncBackend
=
Syncroton_Registry
::
getSyncStateBackend
();
$folderBackend
=
Syncroton_Registry
::
getFolderBackend
();
$contentBackend
=
Syncroton_Registry
::
getContentStateBackend
();
$syncFolder
=
$folderBackend
->
getFolder
(
$this
->
device
->
id
,
$folderId
);
$syncState
=
$syncBackend
->
getSyncState
(
$this
->
device
->
id
,
$syncFolder
->
id
);
$contentBackend
->
create
(
new
Syncroton_Model_Content
(
array
(
'device_id'
=>
$this
->
device
->
id
,
'folder_id'
=>
$syncFolder
->
id
,
'contentid'
=>
$this
->
serverId
(
$event
[
'uid'
],
$folder
),
'creation_time'
=>
$syncState
->
lastsync
,
'creation_synckey'
=>
$syncState
->
counter
,
)));
}
catch
(
Exception
$e
)
{
// ignore
}
}
}
else
{
if
(
$dtstamp
)
{
$this
->
setKolabDataItem
(
$existing
,
self
::
KEY_RESPONSE_DTSTAMP
,
$dtstamp
);
}
$folder
=
$this
->
update_event
(
$event
,
$existing
,
$status
,
$request
->
instanceId
);
}
if
(!
$folder
)
{
throw
new
Syncroton_Exception_Status_MeetingResponse
(
Syncroton_Exception_Status_MeetingResponse
::
MEETING_ERROR
);
}
// TODO: ActiveSync version >= 16, send the iTip response.
if
(
isset
(
$request
->
sendResponse
))
{
// SendResponse can contain Body to use as email body (can be empty)
// TODO: Activesync >= 16.1 proposedStartTime and proposedEndTime.
}
}
// FIXME: We should not return an UID when status=DECLINED
// as it's expected by the specification. Server
// should delete an event in such a case, but we
// keep the event copy with appropriate attendee status instead.
return
empty
(
$status
)
?
null
:
$this
->
serverId
(
$event
[
'uid'
],
$folder
);
}
/**
* Get an event from the invitation email or calendar folder
*/
protected
function
get_event_from_invitation
(
Syncroton_Model_MeetingResponse
$request
)
{
// Limitation: LongId might be used instead of RequestId, this is not supported
if
(
$request
->
requestId
)
{
$mail_class
=
new
kolab_sync_data_email
(
$this
->
device
,
$this
->
syncTimeStamp
);
// Event from an invitation email
if
(
$event
=
$mail_class
->
get_invitation_event
(
$request
->
requestId
))
{
// find the event in calendar
$existing
=
$this
->
find_event_by_uid
(
$event
[
'uid'
]);
return
array
(
$event
,
$existing
);
}
// Event from calendar folder
if
(
$event
=
$this
->
getObject
(
$request
->
collectionId
,
$request
->
requestId
,
$folder
))
{
return
array
(
$event
,
$event
);
}
throw
new
Syncroton_Exception_Status_MeetingResponse
(
Syncroton_Exception_Status_MeetingResponse
::
INVALID_REQUEST
);
}
throw
new
Syncroton_Exception_Status_MeetingResponse
(
Syncroton_Exception_Status_MeetingResponse
::
MEETING_ERROR
);
}
/**
* Find the Kolab event in any (of subscribed personal calendars) folder
*/
protected
function
find_event_by_uid
(
$uid
)
{
if
(
empty
(
$uid
))
{
return
;
}
// TODO: should we check every existing event folder even if not subscribed for sync?
foreach
(
$this
->
listFolders
()
as
$folder
)
{
$storage_folder
=
$this
->
getFolderObject
(
$folder
[
'imap_name'
]);
if
(
$storage_folder
->
get_namespace
()
==
'personal'
&&
(
$result
=
$storage_folder
->
get_object
(
$uid
))
)
{
return
$result
;
}
}
}
/**
* Wrapper to update an event object
*/
protected
function
update_event
(
$event
,
$old
,
$status
,
$instanceId
=
null
)
{
// TODO: instanceId - DateTime - of the exception to be processed, if not set process all occurrences
if
(
$instanceId
)
{
throw
new
Syncroton_Exception_Status_MeetingResponse
(
Syncroton_Exception_Status_MeetingResponse
::
INVALID_REQUEST
);
}
if
(
$event
[
'free_busy'
])
{
$old
[
'free_busy'
]
=
$event
[
'free_busy'
];
}
// Updating an existing event is most-likely a response
// to an iTip request with bumped SEQUENCE
$old
[
'sequence'
]
+=
1
;
// Update the event
return
$this
->
save_event
(
$old
,
$status
);
}
/**
* Save the Kolab event (create if not exist)
* If an event does not exist it will be created in the default folder
*/
protected
function
save_event
(&
$event
,
$status
=
null
)
{
// Find default folder to which we'll save the event
if
(!
isset
(
$event
[
'_mailbox'
]))
{
$folders
=
$this
->
listFolders
();
$storage
=
rcube
::
get_instance
()->
get_storage
();
// find the default
foreach
(
$folders
as
$folder
)
{
if
(
$folder
[
'type'
]
==
8
&&
$storage
->
folder_namespace
(
$folder
[
'imap_name'
])
==
'personal'
)
{
$event
[
'_mailbox'
]
=
$folder
[
'imap_name'
];
break
;
}
}
// if there's no folder marked as default, use any
if
(!
isset
(
$event
[
'_mailbox'
])
&&
!
empty
(
$folders
))
{
foreach
(
$folders
as
$folder
)
{
if
(
$storage
->
folder_namespace
(
$folder
[
'imap_name'
])
==
'personal'
)
{
$event
[
'_mailbox'
]
=
$folder
[
'imap_name'
];
break
;
}
}
}
// TODO: what if the user has no subscribed event folders for this device
// should we use any existing event folder even if not subscribed for sync?
}
if
(
$status
)
{
$this
->
update_attendee_status
(
$event
,
$status
);
}
// TODO: Free/busy trigger?
if
(
isset
(
$event
[
'_mailbox'
]))
{
$folder
=
$this
->
getFolderObject
(
$event
[
'_mailbox'
]);
if
(
$folder
&&
$folder
->
valid
&&
$folder
->
save
(
$event
))
{
return
$folder
;
}
}
return
false
;
}
/**
* Update the attendee status of the user
*/
protected
function
update_attendee_status
(&
$event
,
$status
)
{
$organizer
=
null
;
$emails
=
$this
->
user_emails
();
foreach
((
array
)
$event
[
'attendees'
]
as
$i
=>
$attendee
)
{
if
(
$attendee
[
'role'
]
==
'ORGANIZER'
)
{
$organizer
=
$attendee
;
}
else
if
(
$attendee
[
'email'
]
&&
in_array_nocase
(
$attendee
[
'email'
],
$emails
))
{
$event
[
'attendees'
][
$i
][
'status'
]
=
$status
;
$event
[
'attendees'
][
$i
][
'rsvp'
]
=
false
;
$event_attendee
=
$attendee
;
}
}
if
(!
$event_attendee
)
{
$this
->
logger
->
warn
(
'MeetingResponse on an event where the user is not an attendee. UID: '
.
$event
[
'uid'
]);
throw
new
Syncroton_Exception_Status_MeetingResponse
(
Syncroton_Exception_Status_MeetingResponse
::
MEETING_ERROR
);
}
}
/**
* 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
)
{
$filter
=
array
(
array
(
'type'
,
'='
,
$this
->
modelName
));
switch
(
$filter_type
)
{
case
Syncroton_Command_Sync
::
FILTER_2_WEEKS_BACK
:
$mod
=
'-2 weeks'
;
break
;
case
Syncroton_Command_Sync
::
FILTER_1_MONTH_BACK
:
$mod
=
'-1 month'
;
break
;
case
Syncroton_Command_Sync
::
FILTER_3_MONTHS_BACK
:
$mod
=
'-3 months'
;
break
;
case
Syncroton_Command_Sync
::
FILTER_6_MONTHS_BACK
:
$mod
=
'-6 months'
;
break
;
}
if
(!
empty
(
$mod
))
{
$dt
=
new
DateTime
(
'now'
,
new
DateTimeZone
(
'UTC'
));
$dt
->
modify
(
$mod
);
$filter
[]
=
array
(
'dtend'
,
'>'
,
$dt
);
}
return
$filter
;
}
/**
* Set MeetingStatus according to event data
*/
protected
function
meeting_status_from_kolab
(
$collection
,
$event
,
&
$result
)
{
// 0 - The event is an appointment, which has no attendees.
// 1 - The event is a meeting and the user is the meeting organizer.
// 3 - This event is a meeting, and the user is not the meeting organizer.
// 5 - The meeting has been canceled and the user was the meeting organizer.
// 7 - The meeting has been canceled. The user was not the meeting organizer.
$status
=
0
;
if
(!
empty
(
$event
[
'attendees'
]))
{
// Find out if the user is an organizer
// TODO: Delegation/aliases support
$user_emails
=
$this
->
user_emails
();
$is_organizer
=
false
;
if
(
$event
[
'organizer'
]
&&
$event
[
'organizer'
][
'email'
])
{
$is_organizer
=
in_array_nocase
(
$event
[
'organizer'
][
'email'
],
$user_emails
);
}
if
(
$event
[
'status'
]
==
'CANCELLED'
)
{
$status
=
$is_organizer
?
5
:
7
;
}
else
{
$status
=
$is_organizer
?
1
:
3
;
}
}
$result
[
'meetingStatus'
]
=
$status
;
}
/**
* Converts libkolab alarms spec. into a number of minutes
*/
protected
function
from_kolab_alarm
(
$event
)
{
if
(
isset
(
$event
[
'valarms'
]))
{
foreach
(
$event
[
'valarms'
]
as
$alarm
)
{
if
(
in_array
(
$alarm
[
'action'
],
array
(
'DISPLAY'
,
'AUDIO'
)))
{
$value
=
$alarm
[
'trigger'
];
break
;
}
}
}
if
(
$value
&&
$value
instanceof
DateTime
)
{
if
(
$event
[
'start'
]
&&
(
$interval
=
$event
[
'start'
]->
diff
(
$value
)))
{
if
(
$interval
->
invert
&&
!
$interval
->
m
&&
!
$interval
->
y
)
{
return
intval
(
round
(
$interval
->
s
/
60
)
+
$interval
->
i
+
$interval
->
h
*
60
+
$interval
->
d
*
60
*
24
);
}
}
}
else
if
(
$value
&&
preg_match
(
'/^([-+]*)[PT]*([0-9]+)([WDHMS])$/'
,
$value
,
$matches
))
{
$value
=
intval
(
$matches
[
2
]);
if
(
$value
&&
$matches
[
1
]
!=
'-'
)
{
return
null
;
}
switch
(
$matches
[
3
])
{
case
'S'
:
$value
=
intval
(
round
(
$value
/
60
));
break
;
case
'H'
:
$value
*=
60
;
break
;
case
'D'
:
$value
*=
24
*
60
;
break
;
case
'W'
:
$value
*=
7
*
24
*
60
;
break
;
}
return
$value
;
}
}
/**
* Converts ActiveSync reminder into libkolab alarms spec.
*/
protected
function
to_kolab_alarm
(
$value
,
$event
)
{
if
(
$value
===
null
||
$value
===
''
)
{
return
(
array
)
$event
[
'valarms'
];
}
$valarms
=
array
();
$unsupported
=
array
();
if
(!
empty
(
$event
[
'valarms'
]))
{
foreach
(
$event
[
'valarms'
]
as
$alarm
)
{
if
(!
$current
&&
in_array
(
$alarm
[
'action'
],
array
(
'DISPLAY'
,
'AUDIO'
)))
{
$current
=
$alarm
;
}
else
{
$unsupported
[]
=
$alarm
;
}
}
}
$valarms
[]
=
array
(
'action'
=>
$current
[
'action'
]
?:
'DISPLAY'
,
'description'
=>
$current
[
'description'
]
?:
''
,
'trigger'
=>
sprintf
(
'-PT%dM'
,
$value
),
);
if
(!
empty
(
$unsupported
))
{
$valarms
=
array_merge
(
$valarms
,
$unsupported
);
}
return
$valarms
;
}
/**
* Sanity checks on event input
*
* @param Syncroton_Model_IEntry &$entry Entry object
*
* @throws Syncroton_Exception_Status_Sync
*/
protected
function
check_event
(
Syncroton_Model_IEntry
&
$entry
)
{
// https://msdn.microsoft.com/en-us/library/jj194434(v=exchg.80).aspx
$now
=
new
DateTime
(
'now'
);
$rounded
=
new
DateTime
(
'now'
);
$min
=
(
int
)
$rounded
->
format
(
'i'
);
$add
=
$min
>
30
?
(
60
-
$min
)
:
(
30
-
$min
);
$rounded
->
add
(
new
DateInterval
(
'PT'
.
$add
.
'M'
));
if
(
empty
(
$entry
->
startTime
)
&&
empty
(
$entry
->
endTime
))
{
// use current time rounded to 30 minutes
$end
=
clone
$rounded
;
$end
->
add
(
new
DateInterval
(
$entry
->
allDayEvent
?
'P1D'
:
'PT30M'
));
$entry
->
startTime
=
$rounded
;
$entry
->
endTime
=
$end
;
}
else
if
(
empty
(
$entry
->
startTime
))
{
if
(
$entry
->
endTime
<
$now
||
$entry
->
endTime
<
$rounded
)
{
throw
new
Syncroton_Exception_Status_Sync
(
Syncroton_Exception_Status_Sync
::
INVALID_ITEM
);
}
$entry
->
startTime
=
$rounded
;
}
else
if
(
empty
(
$entry
->
endTime
))
{
if
(
$entry
->
startTime
<
$now
)
{
throw
new
Syncroton_Exception_Status_Sync
(
Syncroton_Exception_Status_Sync
::
INVALID_ITEM
);
}
$rounded
->
add
(
new
DateInterval
(
$entry
->
allDayEvent
?
'P1D'
:
'PT30M'
));
$entry
->
endTime
=
$rounded
;
}
}
/**
* Check if the new event version has any significant changes
*/
protected
function
has_significant_changes
(
$event
,
$old
)
{
// Calendar namespace fields
foreach
(
array
(
'allday'
,
'start'
,
'end'
,
'location'
,
'recurrence'
)
as
$key
)
{
if
(
$event
[
$key
]
!=
$old
[
$key
])
{
// Comparing recurrence is tricky as there can be differences in default
// value handling. Let's try to handle most common cases
if
(
$key
==
'recurrence'
&&
$this
->
fixed_recurrence
(
$event
)
==
$this
->
fixed_recurrence
(
$old
))
{
continue
;
}
return
true
;
}
}
if
(
count
(
$event
[
'attendees'
])
!=
count
(
$old
[
'attendees'
]))
{
return
true
;
}
foreach
(
$event
[
'attendees'
]
as
$idx
=>
$attendee
)
{
$old_attendee
=
$old
[
'attendees'
][
$idx
];
if
(
$old_attendee
[
'email'
]
!=
$attendee
[
'email'
]
||
(
$attendee
[
'role'
]
!=
'ORGANIZER'
&&
$attendee
[
'status'
]
!=
$old_attendee
[
'status'
]
&&
$attendee
[
'status'
]
==
'NEEDS-ACTION'
)
)
{
return
true
;
}
}
return
false
;
}
/**
* Unify recurrence spec. for comparison
*/
protected
function
fixed_recurrence
(
$event
)
{
$rec
=
(
array
)
$event
[
'recurrence'
];
// Add BYDAY if not exists
if
(
$rec
[
'FREQ'
]
==
'WEEKLY'
&&
empty
(
$rec
[
'BYDAY'
]))
{
$days
=
array
(
'SU'
,
'MO'
,
'TU'
,
'WE'
,
'TH'
,
'FR'
,
'SA'
);
$day
=
$event
[
'start'
]->
format
(
'w'
);
$rec
[
'BYDAY'
]
=
$days
[
$day
];
}
if
(!
$rec
[
'INTERVAL'
])
{
$rec
[
'INTERVAL'
]
=
1
;
}
ksort
(
$rec
);
return
$rec
;
}
/**
* Check if the event update request is a fake (for Outlook)
*/
protected
function
isDummyOutlookUpdate
(
$data
,
$entry
,
&
$result
)
{
$is_dummy
=
false
;
// Outlook 2013 sends a dummy update just after MeetingResponse has been processed,
// this update resets attendee status set in the MeetingResponse request.
// We ignore attendees data in such updates, they should not happen according to
// https://msdn.microsoft.com/en-us/library/office/hh428685(v=exchg.140).aspx
// but they will contain some data as alarms and free/busy status so we don't
// ignore them completely
if
(!
empty
(
$entry
)
&&
!
empty
(
$data
->
attendees
)
&&
stripos
(
$this
->
device
->
devicetype
,
'outlook'
)
!==
false
)
{
// Some of these requests use just dummy Timezone
$dummy_tz
=
str_repeat
(
'A'
,
230
)
.
'=='
;
if
(
$data
->
timezone
==
$dummy_tz
)
{
$is_dummy
=
true
;
}
// But some of them do not, so we have check if that is a first
// update immediately (up to 5 seconds) after MeetingResponse request
if
(!
$is_dummy
&&
(
$dtstamp
=
$this
->
getKolabDataItem
(
$entry
,
self
::
KEY_RESPONSE_DTSTAMP
)))
{
$dtstamp
=
new
DateTime
(
$dtstamp
);
$now
=
new
DateTime
(
'now'
,
new
DateTimeZone
(
'UTC'
));
$is_dummy
=
$now
->
getTimestamp
()
-
$dtstamp
->
getTimestamp
()
<=
5
;
}
$this
->
unsetKolabDataItem
(
$result
,
self
::
KEY_RESPONSE_DTSTAMP
);
}
return
$is_dummy
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Apr 5, 11:28 PM (1 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e0/1f/4c027a55f55c825a8e92dc5c6cff
Default Alt Text
kolab_sync_data_calendar.php (39 KB)
Attached To
Mode
rS syncroton
Attached
Detach File
Event Timeline