- fixed ORGANIZER/ATTENDEE in iCal for CalDAV:

+ do NOT use ORGANIZER for events without further participants or a different organizer
+ do not include event owner/ORGANIZER as participant in his own calendar, if he is only participant
--> all other cases include ORGANZIER and additional as ATTENDEE (tested with iCal on iOS and OS X)
- implemented schedule-tag and If-Schedule-Tag-Match header from CalDAV Scheduling
- allow to change participant status and add/remove alarms with schedule-tag instead of ETag
--> If-Schedule-Tag-Match header has precedence over If-Match (ETag) header, but limits changes to participant status and alarms
--> ToDo: test accepting, rejecting recurrences
This commit is contained in:
Ralf Becker 2011-10-20 20:10:04 +00:00
parent e0690d2342
commit 8096c34bef
4 changed files with 126 additions and 22 deletions

View File

@ -1915,10 +1915,11 @@ class calendar_bo
* Get the etag for an entry
*
* @param array|int|string $event array with event or cal_id, or cal_id:recur_date for virtual exceptions
* @param string &$schedule_tag=null on return schedule-tag (egw_cal.cal_id:egw_cal.cal_etag, no participant modifications!)
* @param boolean $client_share_uid_excpetions Does client understand exceptions to be included in VCALENDAR component of series master sharing its UID
* @return string|boolean string with etag or false
*/
function get_etag($entry,$client_share_uid_excpetions=true)
function get_etag($entry, &$schedule_tag=null, $client_share_uid_excpetions=true)
{
if (!is_array($entry))
{
@ -1926,7 +1927,7 @@ class calendar_bo
if (!$this->check_perms(EGW_ACL_FREEBUSY, $entry, 0, 'server')) return false;
$entry = $this->read($entry, $recur_date, true, 'server');
}
$etag = $entry['id'].':'.$entry['etag'];
$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
// use new MAX(modification date) of egw_cal_user table (deals with virtual exceptions too)
if (isset($entry['max_user_modified']))
@ -1955,7 +1956,7 @@ class calendar_bo
{
if ($recurrence['reference'] && $recurrence['id'] != $entry['id']) // ignore series master
{
$etag .= ':'.$this->get_etag($recurrence);
$etag .= ':'.$this->get_etag($recurrence, $full_etag);
}
}
}

View File

@ -238,18 +238,31 @@ class calendar_groupdav extends groupdav_handler
foreach($events as $event)
{
$event['max_user_modified'] = $max_user_modified[$event['id']];
$etag = $this->get_etag($event, $schedule_tag);
//header('X-EGROUPWARE-EVENT-'.$event['id'].': '.$event['title'].': '.date('Y-m-d H:i:s',$event['start']).' - '.date('Y-m-d H:i:s',$event['end']));
$props = array(
'getcontenttype' => HTTP_WebDAV_Server::mkprop('getcontenttype', $this->agent != 'kde' ?
'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar'),
'getcontenttype' => $this->agent != 'kde' ? 'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar',
'getetag' => '"'.$etag.'"',
'schedule-tag' => HTTP_WebDAV_Server::mkprop(groupdav::CALDAV, 'schedule-tag', '"'.$schedule_tag.'"'),
);
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
if ($calendar_data)
{
$content = $this->iCal($event, $filter['users'], strpos($path, '/inbox/') !== false ? 'PUBLISH' : null);
$props['getcontentlength'] = bytes($content);
$props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content);
$props['calendar-data'] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content);
}
/* Calendarserver reports new events with schedule-changes: action: create, which iCal request
* adding it, unfortunately does not lead to showing the new event in the users inbox
if (strpos($path, '/inbox/') !== false && $this->groupdav->prop_requested('schedule-changes'))
{
$props['schedule-changes'] = HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'schedule-changes',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'dtstamp',gmdate('Ymd\THis',$event['created']).'Z'),
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'action',array(
HTTP_WebDAV_Server::mkprop(groupdav::CALENDARSERVER,'create',''),
)),
));
}*/
$files[] = $this->add_resource($path, $event, $props);
}
}
@ -393,7 +406,9 @@ class calendar_groupdav extends groupdav_handler
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'PUBLISH' : null);
$options['mimetype'] = 'text/calendar; charset=utf-8';
header('Content-Encoding: identity');
header('ETag: "'.$this->get_etag($event).'"');
header('ETag: "'.$this->get_etag($event, $schedule_tag).'"');
header('Schedule-Tag: "'.$schedule_tag.'"');
return true;
}
@ -516,7 +531,8 @@ class calendar_groupdav extends groupdav_handler
if (!$prefix) $user = null; // /infolog/ does not imply setting the current user (for new entries it's done anyway)
$return_no_access = true; // as handled by importVCal anyway and allows it to set the status for participants
$oldEvent = $this->_common_get_put_delete('PUT',$options,$id,$return_no_access);
$oldEvent = $this->_common_get_put_delete('PUT',$options,$id,$return_no_access,
isset($_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH'])); // dont fail with 412 Precondition Failed in that case
if (!is_null($oldEvent) && !is_array($oldEvent))
{
if ($this->debug) error_log(__METHOD__.': '.print_r($oldEvent,true).function_backtrace());
@ -556,6 +572,39 @@ class calendar_groupdav extends groupdav_handler
if (is_array($oldEvent))
{
$eventId = $oldEvent['id'];
// client specified a CalDAV Scheduling schedule-tag precondition
if (isset($_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH']))
{
$schedule_tag_match = $_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH'];
if ($schedule_tag_match[0] == '"') $schedule_tag_match = substr($schedule_tag_match, 1, -1);
$this->get_etag($oldEvent, $schedule_tag);
if ($schedule_tag_match !== $schedule_tag)
{
return '412 Precondition Failed';
}
// update only participant status and alarms of current user
if (($events = $handler->icaltoegw($vCalendar)))
{
// todo check behavior for recuring events
foreach($events as $event)
{
if ($this->debug) error_log(__METHOD__."(, $id, $user, '$prefix') eventId=$eventId, user=$user, event=".array2string($event));
if ($event['participants'][$user] != $oldEvent['participants'][$user] &&
!$this->bo->set_status($eventId, $user, $event['participants'][$user], $event['recurrence']))
{
if ($this->debug) error_log(__METHOD__."(,,$user) failed to set_status($eventId, $user, '{$event['participants'][$user]}')");
return '403 Forbidden';
}
if ($this->debug) error_log(__METHOD__."() set_status($eventId, $user, ".array2string($event['participants'][$user])." , $event[recurrence])");
// import alarms
$this->sync_alarms($eventId, (array)$event['alarm'], (array)$oldEvent['alarm'], $user, $event['start']);
}
header('Schedule-Tag: "'.$schedule_tag.'"');
return '204 No Content';
}
}
if ($return_no_access)
{
$retval = true;
@ -599,8 +648,10 @@ class calendar_groupdav extends groupdav_handler
}
}
$etag = $this->get_etag($cal_id, $schedule_tag);
// we should not return an etag here, as we never store the PUT ical byte-by-byte
//header('ETag: "'.$this->get_etag($cal_id).'"');
//header('ETag: "'.$etag.'"');
header('Schedule-Tag: "'.$schedule_tag.'"');
// send GroupDAV Location header only if we dont use caldav_name as path-attribute
if ($retval !== true && self::$path_attr != 'caldav_name')
@ -612,6 +663,49 @@ class calendar_groupdav extends groupdav_handler
return $retval;
}
/**
* Sync alarms of current user: add alarms added on client and remove the ones removed
*
* @param int $cal_id of event to set alarms
* @param array $alarms
* @param array $old_alarms
* @param int $user account_id of user to create alarm for
* @param int $start start-time of event
* @ToDo store other alarm properties like: ACTION, DESCRIPTION, X-WR-ALARMUID
*/
private function sync_alarms($cal_id, array $alarms, array $old_alarms, $user, $start)
{
if ($this->debug) error_log(__METHOD__."($cal_id, ".array2string($alarms).', '.array2string($old_alarms).", $user, $start)");
// todo import alarms
foreach($alarms as $alarm)
{
if ($alarm['owner'] != $this->user) continue; // only import alarms of current user
// check if alarm is already stored or from other users
foreach($old_alarms as $id => $old_alarm)
{
if ($old_alarm['owner'] != $user || $alarm['offset'] == $old_alarm['offset'])
{
unset($old_alarms[$id]); // remove alarms of other user, or already existing alarms
}
}
// alarm not found --> add it
if ($alarm['offset'] != $old_alarm['offset'] || $old_alarm['owner'] != $user)
{
$alarm['owner'] = $user;
$alarm['time'] = $start - $alarm['offset'];
if ($this->debug) error_log(__METHOD__."() adding new alarm from client ".array2string($alarm));
$this->bo->save_alarm($cal_id, $alarm);
}
}
// remove all old alarms left from current user
foreach($old_alarms as $id => $old_alarm)
{
if ($this->debug) error_log(__METHOD__."() deleting alarm '$id' deleted on client ".array2string($old_alarm));
$this->bo->delete_alarm($id);
}
}
/**
* Handle post request for a schedule entry
*
@ -982,14 +1076,14 @@ class calendar_groupdav extends groupdav_handler
}
/**
* Get the etag for an entry, reimplemented to include the participants and stati in the etag
* Get the etag for an entry
*
* @param array/int $event array with event or cal_id
* @return string/boolean string with etag or false
* @param array|int $event array with event or cal_id
* @return string|boolean string with etag or false
*/
function get_etag($entry)
function get_etag($entry, &$schedule_tag=null)
{
$etag = $this->bo->get_etag($entry,$this->client_shared_uid_exceptions);
$etag = $this->bo->get_etag($entry, $schedule_tag, $this->client_shared_uid_exceptions);
//error_log(__METHOD__ . "($entry[id] ($entry[etag]): $entry[title] --> etag=$etag");
return $etag;
@ -999,7 +1093,7 @@ class calendar_groupdav extends groupdav_handler
* Check if user has the neccessary rights on an event
*
* @param int $acl EGW_ACL_READ, EGW_ACL_EDIT or EGW_ACL_DELETE
* @param array/int $event event-array or id
* @param array|int $event event-array or id
* @return boolean null if entry does not exist, false if no access, true if access permitted
*/
function check_access($acl,$event)

View File

@ -423,13 +423,14 @@ class calendar_ical extends calendar_boupdate
switch ($icalFieldName)
{
case 'ATTENDEE':
$attendees = count($event['participants']);
foreach ((array)$event['participants'] as $uid => $status)
{
calendar_so::split_status($status, $quantity, $role);
if ($attendees == 1 &&
$uid == $this->user && $status == 'A') continue;
// do not include event owner/ORGANIZER as participant in his own calendar, if he is only participant
if (count($event['participants']) == 1 && $event['owner'] == $uid) continue;
if (!($info = $this->resource_info($uid))) continue;
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
@ -521,7 +522,6 @@ class calendar_ical extends calendar_boupdate
break;
case 'ORGANIZER':
// according to iCalendar standard, ORGANIZER not used for events in the own calendar
if (!$organizerCN)
{
$organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname')
@ -551,7 +551,8 @@ class calendar_ical extends calendar_boupdate
$parameters['ATTENDEE'][] = $options;
}
}
if ($this->productManufacturer != 'groupdav' || !$this->check_perms(EGW_ACL_EDIT,$event))
// do NOT use ORGANIZER for events without further participants or a different organizer
if (count($event['participants']) > 1 || !isset($event['participants'][$event['owner']]))
{
$attributes['ORGANIZER'] = $organizerURL;
$parameters['ORGANIZER']['CN'] = $organizerCN;
@ -1317,6 +1318,13 @@ class calendar_ical extends calendar_boupdate
$event['owner'] = $event_info['stored_event']['owner'];
}
$event['caldav_name'] = $event_info['stored_event']['caldav_name'];
// as we no longer export event owner/ORGANIZER as only participant, we have to re-add owner as participant
// to not loose him, as EGroupware knows events without owner/ORGANIZER as participant
if (isset($event_info['stored_event']['participants'][$event['owner']]) && !isset($event['participant'][$event['owner']]))
{
$event['participant'][$event['owner']] = $event_info['stored_event']['participants'][$event['owner']];
}
}
else // common adjustments for new events
{

View File

@ -244,9 +244,10 @@ abstract class groupdav_handler
* @param array &$options
* @param int|string &$id on return self::$path_extension got removed
* @param boolean &$return_no_access=false if set to true on call, instead of '403 Forbidden' the entry is returned and $return_no_access===false
* @param boolean $ignore_if_match=false if true, ignore If-Match precondition
* @return array|string entry on success, string with http-error-code on failure, null for PUT on an unknown id
*/
function _common_get_put_delete($method,&$options,&$id,&$return_no_access=false)
function _common_get_put_delete($method,&$options,&$id,&$return_no_access=false,$ignore_if_match=false)
{
if (self::$path_extension) $id = basename($id,self::$path_extension);
@ -275,7 +276,7 @@ abstract class groupdav_handler
$etag = $this->get_etag($entry);
// If the clients sends an "If-Match" header ($_SERVER['HTTP_IF_MATCH']) we check with the current etag
// of the calendar --> on failure we return 412 Precondition failed, to not overwrite the modifications
if (isset($_SERVER['HTTP_IF_MATCH']))
if (isset($_SERVER['HTTP_IF_MATCH']) && !$ignore_if_match)
{
$this->http_if_match = $_SERVER['HTTP_IF_MATCH'];
// strip of quotes around etag, if they exist, that way we allow etag with and without quotes