Refactor identification of recurring event instances (#4722):

- All instances of a recurring series have -YmdTHis appended to their ID
- In 'all' savemode, the master event identified by UID is loaded and updated
- kolab_driver::update_event() returns the UID of the master event in 'all' mode.
  This is then used to send iTip messages for the entire series
This commit is contained in:
Thomas Bruederli 2015-02-27 17:52:17 +01:00
parent 414a571af0
commit 94260b2aeb
3 changed files with 127 additions and 112 deletions

View file

@ -1175,9 +1175,13 @@ class calendar extends rcube_plugin
$event['attendees'] = $master['attendees']; // this tricks us into the next if clause
}
// delete old reference if saved as new
if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') {
$old = null;
}
$event['id'] = $success;
$event['_savemode'] = 'all';
$old = null;
}
// send out notifications
@ -1566,6 +1570,9 @@ class calendar extends rcube_plugin
$filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
if (!empty($event_id)) {
if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id))) {
if ($event['recurrence_id']) {
$event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']));
}
$events = array($event);
$filename = asciiwords($event['title']);
if (empty($filename))
@ -2276,9 +2283,9 @@ class calendar extends rcube_plugin
public static function event_diff($a, $b)
{
$diff = array();
$ignore = array('changed' => 1, 'attachments' => 1, '_notify' => 1, '_owner' => 1, '_savemode' => 1);
$ignore = array('changed' => 1, 'attachments' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
if (!$ignore[$key] && $key[0] != '_' && $a[$key] != $b[$key])
$diff[] = $key;
}

View file

@ -187,7 +187,7 @@ class kolab_calendar extends kolab_storage_folder_api
{
// directly access storage object
if (!$this->events[$id] && ($record = $this->storage->get_object($id)))
$this->events[$id] = $this->_to_rcube_event($record);
$this->events[$id] = $this->_to_driver_event($record, true);
// event not found, maybe a recurring instance is requested
if (!$this->events[$id]) {
@ -195,17 +195,16 @@ class kolab_calendar extends kolab_storage_folder_api
$instance_id = substr($id, strlen($master_id) + 1);
if ($master_id != $id && ($record = $this->storage->get_object($master_id))) {
$master = $this->events[$master_id] = $this->_to_rcube_event($record);
$master = $this->_to_driver_event($record);
}
// check for match on the first instance already
if ($master['_instance'] && $master['_instance'] == $instance_id) {
$this->events[$id] = $this->events[$master_id];
$this->events[$id] = $master;
}
// check for match in top-level exceptions (aka loose single occurrences)
else if ($master && $master['_formatobj'] && ($instance = $master['_formatobj']->get_instance($instance_id))) {
$instance = $this->_to_rcube_event($instance);
$this->events[$instance['id']] = $instance;
$this->events[$id] = $this->_to_driver_event($instance);
}
else if ($master && is_array($master['recurrence'])) {
$this->get_recurring_events($record, $master['start'], null, $id);
@ -294,12 +293,13 @@ class kolab_calendar extends kolab_storage_folder_api
$events = array();
foreach ($this->storage->select($query) as $record) {
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
$event = $this->_to_driver_event($record, !$virtual);
// remember seen categories
if ($event['categories'])
$this->categories[$event['categories']]++;
if ($event['categories']) {
$cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
$this->categories[$cat]++;
}
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
@ -321,11 +321,11 @@ class kolab_calendar extends kolab_storage_folder_api
// find and merge exception for the first instance
if ($virtual && !empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) {
$event_date = $event['start']->format('Ymd');
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
$exdate = $exception['recurrence_date'] ? $exception['recurrence_date']->format('Ymd') : substr($exception['_instance'], 0, 8);
if ($exdate == $event_date) {
$event['_instance'] = $exception['_instance'];
if ($event['_instance'] == $exception['_instance']) {
// clone date objects from main event before adjusting them with exception data
if (is_object($event['start'])) $event['start'] = clone $record['start'];
if (is_object($event['end'])) $event['end'] = clone $record['end'];
kolab_driver::merge_exception_data($event, $exception);
}
}
@ -342,7 +342,7 @@ class kolab_calendar extends kolab_storage_folder_api
// add top-level exceptions (aka loose single occurrences)
else if (is_array($record['exceptions'])) {
foreach ($record['exceptions'] as $ex) {
$component = $this->_to_rcube_event($ex);
$component = $this->_to_driver_event($ex);
if ($component['start'] <= $end && $component['end'] >= $start) {
$events[] = $component;
}
@ -445,7 +445,7 @@ class kolab_calendar extends kolab_storage_folder_api
unset($event['links']);
//generate new event from RC input
$object = $this->_from_rcube_event($event);
$object = $this->_from_driver_event($event);
$saved = $this->storage->save($object, 'event');
if (!$saved) {
@ -460,8 +460,7 @@ class kolab_calendar extends kolab_storage_folder_api
// save links in configuration.relation object
$this->save_links($event['uid'], $links);
$event['id'] = $event['uid'];
$this->events = array($event['uid'] => $this->_to_rcube_event($object));
$this->events = array($event['uid'] => $this->_to_driver_event($object, true));
}
return $saved;
@ -485,7 +484,7 @@ class kolab_calendar extends kolab_storage_folder_api
$links = $event['links'];
unset($event['links']);
$object = $this->_from_rcube_event($event, $old);
$object = $this->_from_driver_event($event, $old);
$saved = $this->storage->save($object, 'event', $old['uid']);
if (!$saved) {
@ -500,7 +499,7 @@ class kolab_calendar extends kolab_storage_folder_api
$this->save_links($event['uid'], $links);
$updated = true;
$this->events = array($event['id'] => $this->_to_rcube_event($object));
$this->events = array($event['uid'] => $this->_to_driver_event($object, true));
// refresh local cache with recurring instances
if ($exception_id) {
@ -626,7 +625,7 @@ class kolab_calendar extends kolab_storage_folder_api
else if (!$exception['_instance'] && is_a($exception['start'], 'DateTime'))
$exception['_instance'] = $exception['start']->format($recurrence_id_format);
$rec_event = $this->_to_rcube_event($exception);
$rec_event = $this->_to_driver_event($exception);
$rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
$rec_event['isexception'] = 1;
@ -675,7 +674,7 @@ class kolab_calendar extends kolab_storage_folder_api
// add to output if in range
$rec_id = $event['uid'] . '-' . $instance_id;
if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $this->_to_rcube_event($next_event);
$rec_event = $this->_to_driver_event($next_event);
$rec_event['_instance'] = $instance_id;
$rec_event['_count'] = $i + 1;
@ -707,7 +706,7 @@ class kolab_calendar extends kolab_storage_folder_api
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_event($record)
private function _to_driver_event($record, $noinst = false)
{
$record['calendar'] = $this->id;
$record['links'] = $this->get_links($record['uid']);
@ -717,17 +716,31 @@ class kolab_calendar extends kolab_storage_folder_api
$record = kolab_driver::add_partstat_class($record, array('NEEDS-ACTION','DECLINED'), $this->get_owner());
}
return kolab_driver::to_rcube_event($record);
// add instance identifier to first occurrence (master event)
$recurrence_id_format = $record['allday'] ? 'Ymd' : 'Ymd\THis';
if (!$noinst && $record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
$record['_instance'] = $record['start']->format($recurrence_id_format);
}
else if (is_a($record['recurrence_date'], 'DateTime')) {
$record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
}
// clean up exception data
if ($record['recurrence'] && is_array($record['recurrence']['EXCEPTIONS'])) {
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
});
}
return $record;
}
/**
* Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
* (opposite of self::_to_rcube_event())
* (opposite of self::_to_driver_event())
*/
private function _from_rcube_event($event, $old = array())
private function _from_driver_event($event, $old = array())
{
$event = kolab_driver::from_rcube_event($event, $old);
// set current user as ORGANIZER
$identity = $this->cal->rc->user->list_emails(true);
if (empty($event['attendees']) && $identity['email'])
@ -750,8 +763,18 @@ class kolab_calendar extends kolab_storage_folder_api
$event['comment'] = $old['comment'];
}
// clean up exception data
if (is_array($event['exceptions'])) {
array_walk($event['exceptions'], function(&$exception) {
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments'],
$event['attachments'], $event['deleted_attachments'], $event['recurrence_id']);
});
}
// remove some internal properties which should not be saved
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'], $event['className']);
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
$event['recurrence_id'], $event['attachments'], $event['deleted_attachments'], $event['className']);
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {

View file

@ -549,9 +549,7 @@ class kolab_driver extends calendar_driver
if ($cal) {
if ($storage = $this->get_calendar($cal)) {
$result = $storage->get_event($id);
if (is_array($result))
self::clean_rcube_event_out($result);
return $result;
return self::to_rcube_event($result);
}
// get event from the address books birthday calendar
else if ($cal == self::BIRTHDAY_CALENDAR_ID) {
@ -562,8 +560,7 @@ class kolab_driver extends calendar_driver
else {
foreach ($this->filter_calendars($writeable, $active, $personal) as $calendar) {
if ($result = $calendar->get_event($id)) {
self::clean_rcube_event_out($result);
return $result;
return self::to_rcube_event($result);
}
}
}
@ -581,10 +578,12 @@ class kolab_driver extends calendar_driver
if (!$this->validate($event))
return false;
$event = self::from_rcube_event($event);
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
if ($storage = $this->get_calendar($cid)) {
// if this is a recurrence instance, append as exception to an already existing object for this UID
if (!empty($event['recurrence_date']) && ($master = $this->get_event($event['uid']))) {
if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
self::add_exception($master, $event);
$success = $storage->update_event($master);
}
@ -611,7 +610,10 @@ class kolab_driver extends calendar_driver
*/
public function edit_event($event)
{
return $this->update_event($event);
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
}
/**
@ -630,12 +632,16 @@ class kolab_driver extends calendar_driver
if ($storage = $this->get_calendar($event['calendar'])) {
$update_event = $storage->get_event($event['recurrence_id']);
$update_event['_savemode'] = $event['_savemode'];
$update_event['id'] = $update_event['uid'];
unset($update_event['recurrence_id']);
self::merge_attendee_data($update_event, $attendees);
}
}
if (($ret = $this->update_attendees($update_event, $attendees)) && $this->rc->config->get('kolab_invitation_calendars')) {
// replace with master event (for iTip reply)
$event = self::to_rcube_event($update_event);
// re-assign to the according (virtual) calendar
if (strtoupper($status) == 'DECLINED')
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
@ -668,7 +674,7 @@ class kolab_driver extends calendar_driver
$saved = false;
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// merge the new event properties onto future exceptions
if ($exception['_instance'] >= $event['_instance']) {
if ($exception['_instance'] >= strval($event['_instance'])) {
self::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
}
// update a specific instance
@ -741,7 +747,7 @@ class kolab_driver extends calendar_driver
{
$success = false;
$savemode = $event['_savemode'];
$decline = $event['decline'];
$decline = $event['_decline'];
if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $savemode;
@ -752,7 +758,7 @@ class kolab_driver extends calendar_driver
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id'] || $event['isexception']) {
$master = $event['recurrence_id'] || $event['isexception'] ? $storage->get_event($event['uid']) : $event;
$master = $storage->get_event($event['uid']);
$savemode = $event['_savemode'] ?: ($event['_instance'] || $event['isexception'] ? 'current' : 'all');
// force 'current' mode for single occurrences stored as exception
@ -768,7 +774,6 @@ class kolab_driver extends calendar_driver
// set event date back to the actual occurrence
if ($exception['recurrence_date'])
$event['start'] = $exception['recurrence_date'];
break;
}
}
@ -782,7 +787,8 @@ class kolab_driver extends calendar_driver
$_SESSION['calendar_restore_event_data'] = $master;
// removing the first instance => just move to next occurence
if ($master['id'] == $event['id'] && $master['recurrence']) {
$recurrence_id_format = $master['allday'] ? 'Ymd' : 'Ymd\THis';
if ($master['recurrence'] && $event['_instance'] == $master['start']->format($recurrence_id_format)) {
$recurring = reset($storage->get_recurring_events($event, $event['start'], null, $event['id'].'-1'));
// no future instances found: delete the master event (bug #1677)
@ -840,7 +846,7 @@ class kolab_driver extends calendar_driver
default: // 'all' is default
// removing the master event with loose exceptions (not recurring though)
if (!empty($event['recurrence_date']) && !empty($master['exceptions'])) {
if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
// make the first exception the new master
$newmaster = array_shift($master['exceptions']);
$newmaster['exceptions'] = $master['exceptions'];
@ -906,9 +912,12 @@ class kolab_driver extends calendar_driver
if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
return false;
$old = $fromcalendar->get_event($event['id']);
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($event['id'], $storage->storage))
if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
return false;
}
$fromcalendar = $storage;
}
@ -919,7 +928,7 @@ class kolab_driver extends calendar_driver
$success = false;
$savemode = 'all';
$attachments = array();
$old = $master = $fromcalendar->get_event($event['id']);
$old = $master = $storage->get_event($event['id']);
if (!$old || !$old['start']) {
rcube::raise_error(array(
@ -930,45 +939,14 @@ class kolab_driver extends calendar_driver
return false;
}
// delete existing attachment(s)
if (!empty($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
if (!empty($old['attachments'])) {
foreach ($old['attachments'] as $idx => $att) {
if ($att['id'] == $attachment) {
$old['attachments'][$idx]['_deleted'] = true;
}
}
}
}
unset($event['deleted_attachments']);
}
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
// skip entries without content (could be existing ones)
if (!$attachment['data'] && !$attachment['path'])
continue;
$attachments[] = array(
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'content' => $attachment['data'],
'path' => $attachment['path'],
);
}
}
$event['attachments'] = array_merge((array)$old['attachments'], $attachments);
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) {
$master = $old['recurrence_id'] || $old['isexception'] ? $fromcalendar->get_event($old['uid']) : $old;
$master = $storage->get_event($old['uid']);
$savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all');
// this-and-future on the first instance equals to 'all'
if (!$old['recurrence_id'] && $savemode == 'future')
$recurrence_id_format = $master['allday'] ? 'Ymd' : 'Ymd\THis';
if ($savemode == 'future' && $master['start'] && $old['_instance'] == $master['start']->format($recurrence_id_format))
$savemode = 'all';
// force 'current' mode for single occurrences stored as exception
else if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception'])
@ -1021,7 +999,7 @@ class kolab_driver extends calendar_driver
// remove recurrence exceptions on re-scheduling
if ($reschedule) {
unset($event['recurrence']['EXCEPTIONS'], $master['recurrence']['EXDATE']);
unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
}
else if (is_array($event['recurrence']['EXCEPTIONS'])) {
// only keep relevant exceptions
@ -1033,6 +1011,8 @@ class kolab_driver extends calendar_driver
return $exdate > $event['start'];
});
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
// compute remaining occurrences
@ -1060,6 +1040,8 @@ class kolab_driver extends calendar_driver
$master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
return $exception['start'] < $event['start'];
});
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
}
if (is_array($master['recurrence']['EXDATE'])) {
$master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function($exdate) use ($event) {
@ -1102,7 +1084,7 @@ class kolab_driver extends calendar_driver
$add_exception = true;
// adjust matching RDATE entry if dates changed
if ($savemode == 'current' && $master['recurrence']['RDATE'] && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $old_date) {
$master['recurrence']['RDATE'][$j] = $event['start'];
@ -1122,7 +1104,7 @@ class kolab_driver extends calendar_driver
break;
default: // 'all' is default
$event['id'] = $master['id'];
$event['id'] = $master['uid'];
$event['uid'] = $master['uid'];
// use start date from master but try to be smart on time or duration changes
@ -1152,7 +1134,7 @@ class kolab_driver extends calendar_driver
}
}
// dates did not change, use the ones from master
else if ($event['start'] == $old['start'] && $event['end'] == $old['end']) {
else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
$event['start'] = $master['start'];
$event['end'] = $master['end'];
}
@ -1198,12 +1180,15 @@ class kolab_driver extends calendar_driver
}
}
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
// unset _dateonly flags in (cached) date objects
unset($event['start']->_dateonly, $event['end']->_dateonly);
$success = $storage->update_event($event);
$success = $storage->update_event($event) ? $event['id'] : false; // return master UID
break;
}
@ -1500,7 +1485,7 @@ class kolab_driver extends calendar_driver
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
array_walk($events, 'kolab_driver::clean_rcube_event_out');
array_walk($events, 'kolab_driver::to_rcube_event');
return $events;
}
@ -1663,8 +1648,8 @@ class kolab_driver extends calendar_driver
$event = $storage->get_event($event['id']);
if ($event && !empty($event['attachments'])) {
foreach ($event['attachments'] as $att) {
if ($event && !empty($event['_attachments'])) {
foreach ($event['_attachments'] as $att) {
if ($att['id'] == $id) {
return $att;
}
@ -1878,12 +1863,22 @@ class kolab_driver extends calendar_driver
/**
* Convert from Kolab_Format to internal representation
* Convert from driver format to external caledar app data
*/
public static function to_rcube_event($record)
public static function to_rcube_event(&$record)
{
if (!is_array($record))
return $record;
$record['id'] = $record['uid'];
if ($record['_instance']) {
$record['id'] .= '-' . $record['_instance'];
if (!$record['recurrence_id'] && !empty($record['recurrence']))
$record['recurrence_id'] = $record['uid'];
}
// all-day events go from 12:00 - 13:00
if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = clone $record['start'];
@ -1932,17 +1927,6 @@ class kolab_driver extends calendar_driver
if (empty($record['recurrence']))
unset($record['recurrence']);
// add instance identifier to first occurrence (master event)
// do not add 'recurrence_date' though in order to keep the master even being exported as such
$recurrence_id_format = $record['allday'] ? 'Ymd' : 'Ymd\THis';
if ($record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
$record['_instance'] = $record['start']->format($recurrence_id_format);
}
else if (is_a($record['recurrence_date'], 'DateTime')) {
$record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
$record['id'] = $record['uid'] . '-' . $record['_instance'];
}
// clean up exception data
if (is_array($record['recurrence']['EXCEPTIONS'])) {
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
@ -1950,16 +1934,10 @@ class kolab_driver extends calendar_driver
});
}
return $record;
}
/**
* Remove some internal properties before sending to event out to the calendar app
*/
public static function clean_rcube_event_out(&$record)
{
unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
$record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']);
return $record;
}
/**
@ -1968,7 +1946,7 @@ class kolab_driver extends calendar_driver
public static function from_rcube_event($event, $old = array())
{
// in kolab_storage attachments are indexed by content-id
if (is_array($event['attachments'])) {
if (is_array($event['attachments']) || !empty($event['deleted_attachments'])) {
$event['_attachments'] = array();
foreach ($event['attachments'] as $attachment) {
@ -1985,7 +1963,7 @@ class kolab_driver extends calendar_driver
}
// flagged for deletion => set to false
if ($attachment['_deleted']) {
if ($attachment['_deleted'] || in_array($attachment['id'], (array)$event['deleted_attachments'])) {
$event['_attachments'][$key] = false;
}
// replace existing entry
@ -1998,7 +1976,14 @@ class kolab_driver extends calendar_driver
}
}
unset($event['attachments']);
$event['_attachments'] = array_merge((array)$old['_attachments'], $event['_attachments']);
// attachments flagged for deletion => set to false
foreach ($event['_attachments'] as $key => $attachment) {
if ($attachment['_deleted'] || in_array($attachment['id'], (array)$event['deleted_attachments'])) {
$event['_attachments'][$key] = false;
}
}
}
return $event;