plugins/calendar plugins/libcalendaring plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Thu Feb 12 10:14:04 CET 2015


 plugins/calendar/drivers/kolab/kolab_calendar.php        |   70 ++++++++-------
 plugins/calendar/drivers/kolab/kolab_driver.php          |   54 ++++++++---
 plugins/libcalendaring/libvcalendar.php                  |   13 +-
 plugins/libcalendaring/tests/libvcalendar.php            |    1 
 plugins/libcalendaring/tests/resources/recurrence-id.ics |    2 
 plugins/libkolab/lib/kolab_format_event.php              |   65 ++++++++++---
 6 files changed, 138 insertions(+), 67 deletions(-)

New commits:
commit ad55fc706d3f5813f6a0f4b0f93b8970046b4d6d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Feb 12 10:08:22 2015 +0100

    Fix handling of Recurrence-ID properties for recurrence exceptions to comply with RFC 5545 (#4385)

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 9d573ee..2b50ac8 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -191,7 +191,7 @@ class kolab_calendar extends kolab_storage_folder_api
 
     // event not found, maybe a recurring instance is requested
     if (!$this->events[$id]) {
-      $master_id = preg_replace('/-\d+$/', '', $id);
+      $master_id = preg_replace('/-\d+(T\d{6})?$/', '', $id);
       if ($master_id != $id && ($record = $this->storage->get_object($master_id)))
         $this->events[$master_id] = $this->_to_rcube_event($record);
 
@@ -455,7 +455,7 @@ class kolab_calendar extends kolab_storage_folder_api
       $this->save_links($event['uid'], $links);
 
       $updated = true;
-      $this->events[$event['id']] = $this->_to_rcube_event($object);
+      $this->events = array($event['id'] => $this->_to_rcube_event($object));
 
       // refresh local cache with recurring instances
       if ($exception_id) {
@@ -564,34 +564,39 @@ class kolab_calendar extends kolab_storage_folder_api
       $end->add(new DateInterval($intvl));
     }
 
-    // add recurrence exceptions to output
-    $i = 0;
+    // 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']);
+
+    // read recurrence exceptions first
     $events = array();
-    $exdates = array();
+    $exdata = 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']);
+    $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
 
+    if (is_array($event['recurrence']['EXCEPTIONS'])) {
       foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
+        if (!$exception['_instance'] && is_a($exception['recurrence_date'], 'DateTime'))
+          $exception['_instance'] = $exception['recurrence_date']->format($recurrence_id_format);
+        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['id'] = $event['uid'] . '-' . ++$i;
-        $rec_event['recurrence_id'] = $event['uid'];
-        $rec_event['recurrence'] = $recurrence_rule;
-        $rec_event['_instance'] = $i;
+        $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
         $rec_event['isexception'] = 1;
-        $events[] = $rec_event;
 
         // found the specifically requested instance, exiting...
         if ($rec_event['id'] == $event_id) {
+          $rec_event['recurrence'] = $recurrence_rule;
+          $rec_event['recurrence_id'] = $event['uid'];
+          $events[] = $rec_event;
           $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'];
+        $exdate = substr($exception['_instance'], 0, 8);
+        $exdata[$exdate] = $rec_event;
         if ($rec_event['thisandfuture']) {
           $futuredata[$exdate] = $rec_event;
         }
@@ -608,27 +613,27 @@ class kolab_calendar extends kolab_storage_folder_api
       $recurrence = new calendar_recurrence($this->cal, $event);
     }
 
+    $i = 0;
     while ($next_event = $recurrence->next_instance()) {
-      // 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;
-      }
+      $datestr = $next_event['start']->format('Ymd');
+      $instance_id = $next_event['start']->format($recurrence_id_format);
+
+      // use this event data for future recurring instances
+      if ($futuredata[$datestr])
+        $overlay_data = $futuredata[$datestr];
 
       // add to output if in range
-      $rec_id = $event['uid'] . '-' . ++$i;
+      $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['_instance'] = $instance_id;
 
-        if ($overlay_data)  // copy data from a 'this-and-future' exception
-          $this->_merge_event_data($rec_event, $overlay_data);
+        if ($overlay_data || $exdata[$datestr])  // copy data from exception
+          $this->_merge_event_data($rec_event, $exdata[$datestr] ?: $overlay_data);
 
         $rec_event['id'] = $rec_id;
         $rec_event['recurrence_id'] = $event['uid'];
-        $rec_event['_instance'] = $i;
+        $rec_event['recurrence'] = $recurrence_rule;
         unset($rec_event['_attendees']);
         $events[] = $rec_event;
 
@@ -641,7 +646,7 @@ class kolab_calendar extends kolab_storage_folder_api
         break;
 
       // avoid endless recursion loops
-      if ($i > 1000)
+      if (++$i > 1000)
           break;
     }
     
@@ -661,8 +666,13 @@ class kolab_calendar extends kolab_storage_folder_api
     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'))
+        if (is_object($event[$prop]) && is_a($event[$prop], 'DateTime')) {
           $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
+          // set date value if overlay is an exception of the current instance
+          if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
+            $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
+          }
+        }
       }
       else if ($prop[0] != '_' && !in_array($prop, $forbidden))
         $event[$prop] = $value;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index b53ffbf..0d5b9ab 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -692,9 +692,14 @@ class kolab_driver extends calendar_driver
 
       // removing an exception instance
       if ($event['recurrence_id']) {
-        $i = $event['_instance'] - 1;
-        if (!empty($master['recurrence']['EXCEPTIONS'][$i])) {
-          unset($master['recurrence']['EXCEPTIONS'][$i]);
+        foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
+          if ($exception['_instance'] == $event['_instance']) {
+            unset($master['recurrence']['EXCEPTIONS'][$i]);
+            // set event date back to the actual occurrence
+            if ($exception['recurrence_date'])
+              $event['start'] = $exception['recurrence_date'];
+            break;
+          }
         }
       }
 
@@ -879,9 +884,11 @@ class kolab_driver extends calendar_driver
     }
 
     // keep saved exceptions (not submitted by the client)
-    if ($old['recurrence']['EXDATE'])
+    if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
       $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
-    if ($old['recurrence']['EXCEPTIONS'])
+    if (isset($event['recurrence']['EXCEPTIONS']))
+      $with_exceptions = true;  // exceptions already provided (e.g. from iCal import)
+    else if ($old['recurrence']['EXCEPTIONS'])
       $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
 
     switch ($savemode) {
@@ -907,17 +914,22 @@ class kolab_driver extends calendar_driver
         $event['recurrence'] = array();
         $event['thisandfuture'] = $savemode == 'future';
 
+        // TODO: increment sequence if scheduling is affected
+
         // remove some internal properties which should not be saved
-        unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
+        unset($event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']);
 
         // 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;
+        if ($old['recurrence_id'] && is_array($master['recurrence']['EXCEPTIONS'])) {
+          foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
+            if ($exception['_instance'] == $old['_instance']) {
+              $event['_instance'] = $old['_instance'];
+              $event['recurrence_date'] = $old['recurrence_date'];
+              $master['recurrence']['EXCEPTIONS'][$i] = $event;
+              $success = $storage->update_event($master, $old['id']);
+              break 2;
             }
+          }
         }
 
         $add_exception = true;
@@ -936,6 +948,7 @@ class kolab_driver extends calendar_driver
 
         // save as new exception to master event
         if ($add_exception) {
+          $event['_instance'] = $old['_instance'];
           $master['recurrence']['EXCEPTIONS'][] = $event;
         }
         $success = $storage->update_event($master);
@@ -955,10 +968,11 @@ class kolab_driver extends calendar_driver
         $new_duration = $event['end']->format('U') - $event['start']->format('U');
         
         $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+        $date_shift = $old['start']->diff($event['start']);
         
         // shifted or resized
         if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
-          $event['start'] = $master['start']->add($old['start']->diff($event['start']));
+          $event['start'] = $master['start']->add($date_shift);
           $event['end'] = clone $event['start'];
           $event['end']->add(new DateInterval('PT'.$new_duration.'S'));
           
@@ -976,6 +990,20 @@ class kolab_driver extends calendar_driver
           $event['end'] = $master['end'];
         }
 
+        // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+        if (($old_start_date != $new_start_date || $old_start_time != $new_start_time) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
+          $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
+          foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+            $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
+                rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+            if (is_a($recurrence_id, 'DateTime')) {
+              $recurrence_id->add($date_shift);
+              $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
+              $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
+            }
+          }
+        }
+
         // unset _dateonly flags in (cached) date objects
         unset($event['start']->_dateonly, $event['end']->_dateonly);
 
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 127ee36..4163cfb 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -318,6 +318,7 @@ class libvcalendar implements Iterator
                     if (!$seen[$object['uid']]++) {
                         // parse recurrence exceptions
                         if ($object['recurrence']) {
+                            $object['recurrence']['EXCEPTIONS'] = array();
                             foreach ($vobject->children as $component) {
                                 if ($component->name == 'VEVENT' && isset($component->{'RECURRENCE-ID'})) {
                                     try {
@@ -455,6 +456,9 @@ class libvcalendar implements Iterator
 
             case 'RECURRENCE-ID':
                 $event['recurrence_date'] = self::convert_datetime($prop);
+                if ($prop->offsetGet('RANGE') == 'THISANDFUTURE' || $prop->offsetGet('THISANDFUTURE') !== null) {
+                    $event['thisandfuture'] = true;
+                }
                 break;
 
             case 'RELATED-TO':
@@ -1151,11 +1155,10 @@ class libvcalendar implements Iterator
         // append recurrence exceptions
         if (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) {
             foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
-                $exdate = clone $event['start'];
-                $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
-                $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, true);
-                // if ($ex['thisandfuture'])  // not supported by any client :-(
-                //    $recurrence_id->add('RANGE', 'THISANDFUTURE');
+                $exdate = $ex['recurrence_date'] ?: $ex['start'];
+                $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, false, (bool)$event['allday']);
+                if ($ex['thisandfuture'])
+                    $recurrence_id->add('RANGE', 'THISANDFUTURE');
                 $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
             }
         }
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index 9e2f03b..f8b0d85 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -164,6 +164,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
         $events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8');
         $this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty");
         $this->assertInstanceOf('DateTime', $events[0]['recurrence_date'], "Recurrence-ID as date");
+        $this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE");
     }
 
     /**
diff --git a/plugins/libcalendaring/tests/resources/recurrence-id.ics b/plugins/libcalendaring/tests/resources/recurrence-id.ics
index 41485f9..8229da2 100644
--- a/plugins/libcalendaring/tests/resources/recurrence-id.ics
+++ b/plugins/libcalendaring/tests/resources/recurrence-id.ics
@@ -22,7 +22,7 @@ DTSTART;TZID="W. Europe":20140230T150000
 DTEND;TZID="W. Europe":20140230T163000
 TRANSP:OPAQUE
 RDATE;TZID="W. Europe";VALUE=PERIOD:20140227T140000/20140227T153000
-RECURRENCE-ID:20140227T130000Z
+RECURRENCE-ID;RANGE=THISANDFUTURE:20140227T130000Z
 SEQUENCE:0
 UID:7e93e8e8eef16f28aa33b78cd73613ebff
 DTSTAMP:20140120T105609Z
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 8cad89a..03b5dde 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -94,13 +94,26 @@ class kolab_format_event extends kolab_format_xcal
         $this->obj->setStatus($status);
 
         // save recurrence exceptions
-        if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
+        if (is_array($object['recurrence']) && is_array($object['recurrence']['EXCEPTIONS'])) {
+            $recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
             $vexceptions = new vectorevent;
             foreach((array)$object['recurrence']['EXCEPTIONS'] as $i => $exception) {
                 $exevent = new kolab_format_event;
                 $exevent->set(($compacted = $this->compact_exception($exception, $object)));  // only save differing values
-                $exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
+
+                // get value for recurrence-id
+                if (!empty($exception['recurrence_date']) && is_a($exception['recurrence_date'], 'DateTime')) {
+                    $recurrence_id = $exception['recurrence_date'];
+                    $compacted['_instance'] = $recurrence_id->format($recurrence_id_format);
+                }
+                else if (!empty($exception['_instance']) && strlen($exception['_instance']) > 4) {
+                    $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $object['start']->getTimezone());
+                    $compacted['recurrence_date'] = $recurrence_id;
+                }
+                $exevent->obj->setRecurrenceID(self::get_datetime($recurrence_id ?: $exception['start'], null,  $object['allday']), (bool)$exception['thisandfuture']);
+
                 $vexceptions->push($exevent->obj);
+
                 // write cleaned-up exception data back to memory/cache
                 $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
             }
@@ -172,15 +185,27 @@ class kolab_format_event extends kolab_format_xcal
         // this is an exception object
         if ($this->obj->recurrenceID()->isValid()) {
             $object['thisandfuture'] = $this->obj->thisAndFuture();
+            $object['recurrence_date'] = self::php_datetime($this->obj->recurrenceID());
         }
         // read exception event objects
         else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
             $recurrence_exceptions = array();
+            $recurrence_id_format = $object['allday'] ? 'Ymd' : 'Ymd\THis';
             for ($i=0; $i < $exceptions->size(); $i++) {
                 if (($exobj = $exceptions->get($i))) {
                     $exception = new kolab_format_event($exobj);
                     if ($exception->is_valid()) {
-                        $recurrence_exceptions[] = $this->expand_exception($exception->to_array(), $object);
+                        $exdata = $exception->to_array();
+
+                        // fix date-only recurrence ID saved by old versions
+                        if ($exdata['recurrence_date'] && $exdata['recurrence_date']->_dateonly && !$object['allday']) {
+                            $exdata['recurrence_date']->setTimezone($object['start']->getTimezone());
+                            $exdata['recurrence_date']->setTime($object['start']->format('G'), intval($object['start']->format('i')), intval($object['start']->format('s')));
+                        }
+
+                        $recurrence_id = $exdata['recurrence_date'] ?: $exdata['start'];
+                        $exdata['_instance'] = $recurrence_id->format($recurrence_id_format);
+                        $recurrence_exceptions[] = $this->expand_exception($exdata, $object);
                     }
                 }
             }
@@ -211,21 +236,21 @@ class kolab_format_event extends kolab_format_xcal
      */
     private function compact_exception($exception, $master)
     {
-      $forbidden = array('recurrence','organizer','attendees','sequence');
+        $forbidden = array('recurrence','organizer','_attachments');
 
-      foreach ($forbidden as $prop) {
-        if (array_key_exists($prop, $exception)) {
-          unset($exception[$prop]);
+        foreach ($forbidden as $prop) {
+            if (array_key_exists($prop, $exception)) {
+                unset($exception[$prop]);
+            }
         }
-      }
 
-      foreach ($master as $prop => $value) {
-        if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
-          unset($exception[$prop]);
+        foreach ($master as $prop => $value) {
+            if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
+                unset($exception[$prop]);
+            }
         }
-      }
 
-      return $exception;
+        return $exception;
     }
 
     /**
@@ -233,12 +258,16 @@ class kolab_format_event extends kolab_format_xcal
      */
     private function expand_exception($exception, $master)
     {
-      foreach ($master as $prop => $value) {
-        if (empty($exception[$prop]) && !empty($value))
-          $exception[$prop] = $value;
-      }
+        foreach ($master as $prop => $value) {
+            if (empty($exception[$prop]) && !empty($value)) {
+                $exception[$prop] = $value;
+                if ($prop == 'recurrence') {
+                    unset($exception[$prop]['EXCEPTIONS']);
+                }
+            }
+        }
 
-      return $exception;
+        return $exception;
     }
 
 }




More information about the commits mailing list