diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 5f4cc6bb..058b8011 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -1096,6 +1096,7 @@ class calendar extends rcube_plugin $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']); if ($event['recurrence']['UNTIL']) $event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'])->format('c'); + unset($event['recurrence']['EXCEPTIONS']); } foreach ((array)$event['attachments'] as $k => $attachment) { @@ -1109,7 +1110,7 @@ class calendar extends rcube_plugin 'title' => strval($event['title']), 'description' => strval($event['description']), 'location' => strval($event['location']), - 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower($event['categories']), true), + 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true), 'allDay' => ($event['allday'] == 1), ) + $event; } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 24d7bd67..f4c7749b 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -507,8 +507,9 @@ function rcube_calendar_ui(settings) // show warning if editing a recurring event if (event.id && event.recurrence) { + var sel = event.thisandfuture ? 'future' : 'all'; $('#edit-recurring-warning').show(); - $('input.edit-recurring-savemode[value="all"]').prop('checked', true); + $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true); } else $('#edit-recurring-warning').hide(); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index a0d0c527..478a08c0 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -46,6 +46,7 @@ * 'COUNT' => 1..n, // number of times * // + more properties (see http://www.kanzaki.com/docs/ical/recur.html) * 'EXDATE' => array(), // list of DateTime objects of exception Dates/Times + * 'EXCEPTIONS' => array(), list of event objects which denote exceptions in the recurrence chain * ), * 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event * 'categories' => 'Event category', diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 45db6383..764f619f 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -312,7 +312,7 @@ class kolab_calendar * @return boolean True on success, False on error */ - public function update_event($event) + public function update_event($event, $exception_id = null) { $updated = false; $old = $this->storage->get_object($event['id']); @@ -333,6 +333,11 @@ class kolab_calendar else { $updated = true; $this->events[$event['id']] = $this->_to_rcube_event($object); + + // refresh local cache with recurring instances + if ($exception_id) { + $this->_get_recurring_events($object, $event['start'], $event['end'], $exception_id); + } } return $updated; @@ -413,26 +418,67 @@ class kolab_calendar $end->add(new DateInterval($intvl)); } + // add recurrence exceptions to output + $i = 0; + $events = array(); + $exdates = array(); + $futuredata = array(); + if (is_array($event['recurrence']['EXCEPTIONS'])) { + // copy the recurrence rule from the master event (to be used in the UI) + $recurrence_rule = $event['recurrence']; + unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']); + + foreach ($event['recurrence']['EXCEPTIONS'] as $exception) { + $rec_event = $this->_to_rcube_event($exception); + $rec_event['id'] = $event['uid'] . '-' . ++$i; + $rec_event['recurrence_id'] = $event['uid']; + $rec_event['recurrence'] = $recurrence_rule; + $rec_event['_instance'] = $i; + $events[] = $rec_event; + + // found the specifically requested instance, exiting... + if ($rec_event['id'] == $event_id) { + $this->events[$rec_event['id']] = $rec_event; + return $events; + } + + // remember this exception's date + $exdate = $rec_event['start']->format('Y-m-d'); + $exdates[$exdate] = $rec_event['id']; + if ($rec_event['thisandfuture']) { + $futuredata[$exdate] = $rec_event; + } + } + } + // use libkolab to compute recurring events if (class_exists('kolabcalendaring')) { $recurrence = new kolab_date_recurrence($object); } else { - // fallback to local recurrence implementation - require_once($this->cal->home . '/lib/calendar_recurrence.php'); - $recurrence = new calendar_recurrence($this->cal, $event); + // fallback to local recurrence implementation + require_once($this->cal->home . '/lib/calendar_recurrence.php'); + $recurrence = new calendar_recurrence($this->cal, $event); } - $i = 0; - $events = array(); while ($next_event = $recurrence->next_instance()) { - $rec_start = $next_event['start']->format('U'); - $rec_end = $next_event['end']->format('U'); - $rec_id = $event['uid'] . '-' . ++$i; + // skip if there's an exception at this date + $datestr = $next_event['start']->format('Y-m-d'); + if ($exdates[$datestr]) { + // use this event data for future recurring instances + if ($futuredata[$datestr]) + $overlay_data = $futuredata[$datestr]; + continue; + } // add to output if in range + $rec_id = $event['uid'] . '-' . ++$i; if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) { $rec_event = $this->_to_rcube_event($next_event); + + if ($overlay_data) // copy data from a 'this-and-future' exception + $this->_merge_event_data($rec_event, $overlay_data); + $rec_event['id'] = $rec_id; $rec_event['recurrence_id'] = $event['uid']; $rec_event['_instance'] = $i; @@ -455,6 +501,27 @@ class kolab_calendar return $events; } + /** + * Merge certain properties from the overlay event to the base event object + * + * @param array The event object to be altered + * @param array The overlay event object to be merged over $event + */ + private function _merge_event_data(&$event, $overlay) + { + static $forbidden = array('id','uid','created','changed','recurrence','organizer','attendees','sequence'); + + foreach ($overlay as $prop => $value) { + // adjust time of the recurring event instance + if ($prop == 'start' || $prop == 'end') { + if (is_object($event[$prop]) && is_a($event[$prop], 'DateTime')) + $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s'))); + } + else if ($prop[0] != '_' && !in_array($prop, $forbidden)) + $event[$prop] = $value; + } + } + /** * Convert from Kolab_Format to internal representation */ diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 886f2802..bd4a8554 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -421,6 +421,14 @@ class kolab_driver extends calendar_driver $savemode = $event['_savemode']; } + // removing an exception instance + if ($event['recurrence_id']) { + $i = $event['_instance'] - 1; + if (!empty($master['recurrence']['EXCEPTIONS'][$i])) { + unset($master['recurrence']['EXCEPTIONS'][$i]); + } + } + switch ($savemode) { case 'current': $_SESSION['calendar_restore_event_data'] = $master; @@ -565,6 +573,8 @@ class kolab_driver extends calendar_driver // keep saved exceptions (not submitted by the client) if ($old['recurrence']['EXDATE']) $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE']; + if ($old['recurrence']['EXCEPTIONS']) + $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS']; switch ($savemode) { case 'new': @@ -580,57 +590,28 @@ class kolab_driver extends calendar_driver $success = $storage->insert_event($event); break; - - case 'current': - // modifying the first instance => just move to next occurence - if ($master['id'] == $event['id']) { - $recurring = reset($storage->_get_recurring_events($event, $event['start'], null, $event['id'].'-1')); - $master['start'] = $recurring['start']; - $master['end'] = $recurring['end']; - if ($master['recurrence']['COUNT']) - $master['recurrence']['COUNT']--; - } - else { // add exception to master event - $master['recurrence']['EXDATE'][] = $old['start']; - } - $storage->update_event($master); - - // insert new event for this occurence - $event += $old; - $event['recurrence'] = array(); - unset($event['recurrence_id']); - $event['uid'] = $this->cal->generate_uid(); - $success = $storage->insert_event($event); - break; - case 'future': - if ($master['id'] != $event['id']) { - // set until-date on master event - $master['recurrence']['UNTIL'] = clone $old['start']; - $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); - unset($master['recurrence']['COUNT']); - $storage->update_event($master); - - // save this instance as new recurring event - $event += $old; - $event['uid'] = $this->cal->generate_uid(); - - // if recurrence COUNT, update value to the correct number of future occurences - if ($event['recurrence']['COUNT']) { - $event['recurrence']['COUNT'] -= $old['_instance']; - } - - // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::insert_event() - if (strlen($event['recurrence']['BYDAY']) == 2) - unset($event['recurrence']['BYDAY']); - if ($master['recurrence']['BYMONTH'] == $master['start']->format('n')) - unset($event['recurrence']['BYMONTH']); - - $success = $storage->insert_event($event); - break; + case 'current': + // recurring instances shall not store recurrence rules + $event['recurrence'] = array(); + $event['thisandfuture'] = $savemode == 'future'; + + // save properties to a recurrence exception instance + if ($old['recurrence_id']) { + $i = $old['_instance'] - 1; + if (!empty($master['recurrence']['EXCEPTIONS'][$i])) { + $master['recurrence']['EXCEPTIONS'][$i] = $event; + $success = $storage->update_event($master, $old['id']); + break; + } } + // save as new exception to master event + $master['recurrence']['EXCEPTIONS'][] = $event; + $success = $storage->update_event($master); + break; + default: // 'all' is default $event['id'] = $master['id']; $event['uid'] = $master['uid']; diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index c78173b4..35255b91 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -109,6 +109,8 @@ class libcalendaring extends rcube_plugin $dt = new DateTime('@'.$td); else if (is_string($dt)) $dt = new DateTime($dt); + else + return $dt; $dt->setTimezone($this->timezone); return $dt; diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php index cd357fc5..2b3f0c8f 100644 --- a/plugins/libkolab/lib/kolab_format_event.php +++ b/plugins/libkolab/lib/kolab_format_event.php @@ -30,6 +30,19 @@ class kolab_format_event extends kolab_format_xcal protected $read_func = 'readEvent'; protected $write_func = 'writeEvent'; + /** + * Default constructor + */ + function __construct($data = null, $version = 3.0) + { + parent::__construct(is_string($data) ? $data : null, $version); + + // got an Event object as argument + if (is_object($data) && is_a($data, $this->objclass)) { + $this->obj = $data; + $this->loaded = true; + } + } /** * Clones into an instance of libcalendaring's extended EventCal class @@ -95,6 +108,18 @@ class kolab_format_event extends kolab_format_xcal $this->obj->setAttachments($vattach); + // save recurrence exceptions + if ($object['recurrence']['EXCEPTIONS']) { + $vexceptions = new vectorevent; + foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) { + $exevent = new kolab_format_event; + $exevent->set($this->compact_exception($exception, $object)); // only save differing values + $exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']); + $vexceptions->push($exevent->obj); + } + $this->obj->setExceptions($vexceptions); + } + // cache this data $this->data = $object; unset($this->data['_formatobj']); @@ -166,6 +191,22 @@ class kolab_format_event extends kolab_format_xcal } } + // read exception event objects + if (($exceptions = $this->obj->exceptions()) && $exceptions->size()) { + for ($i=0; $i < $exceptions->size(); $i++) { + if (($exobj = $exceptions->get($i))) { + $exception = new kolab_format_event($exobj); + if ($exception->is_valid()) { + $object['recurrence']['EXCEPTIONS'][] = $this->expand_exception($exception->to_array(), $object); + } + } + } + } + // this is an exception object + else if ($this->obj->recurrenceID()->isValid()) { + $object['thisandfuture'] = $this->obj->thisAndFuture(); + } + // merge with additional data, e.g. attachments from the message if ($data) { foreach ($data as $idx => $value) { @@ -201,4 +242,34 @@ class kolab_format_event extends kolab_format_xcal return $tags; } + /** + * Remove some attributes from the exception container + */ + private function compact_exception($exception, $master) + { + static $forbidden = array('recurrence','organizer','attendees','sequence'); + + $out = $exception; + foreach ($exception as $prop => $val) { + if (in_array($prop, $forbidden)) { + unset($out[$prop]); + } + } + + return $out; + } + + /** + * Copy attributes not specified by the exception from the master event + */ + private function expand_exception($exception, $master) + { + foreach ($master as $prop => $value) { + if (empty($exception[$prop]) && !empty($value)) + $exception[$prop] = $value; + } + + return $exception; + } + } diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php index b3d3c275..bbe3404c 100644 --- a/plugins/libkolab/lib/kolab_format_xcal.php +++ b/plugins/libkolab/lib/kolab_format_xcal.php @@ -113,7 +113,7 @@ abstract class kolab_format_xcal extends kolab_format ); // read organizer and attendees - if ($organizer = $this->obj->organizer()) { + if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) { $object['organizer'] = array( 'email' => $organizer->email(), 'name' => $organizer->name(), @@ -170,9 +170,9 @@ abstract class kolab_format_xcal extends kolab_format $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth)); } - if ($exceptions = $this->obj->exceptionDates()) { - for ($i=0; $i < $exceptions->size(); $i++) { - if ($exdate = self::php_datetime($exceptions->get($i))) + if ($exdates = $this->obj->exceptionDates()) { + for ($i=0; $i < $exdates->size(); $i++) { + if ($exdate = self::php_datetime($exdates->get($i))) $object['recurrence']['EXDATE'][] = $exdate; } }