Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F6065801
kolab_sync_data_calendar.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
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
Fri, May 22, 4:44 AM (1 d, 23 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
783102
Default Alt Text
kolab_sync_data_calendar.php (39 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment