More complete support for reccuring event exceptions when dealing with iCalendar...
authorMichael J. Rubinsky <mrubinsk@horde.org>
Wed, 7 Jul 2010 22:53:29 +0000 (18:53 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Wed, 7 Jul 2010 23:01:50 +0000 (19:01 -0400)
Bug: 9091

framework/iCalendar/iCalendar.php
kronolith/lib/Api.php
kronolith/lib/Event.php
kronolith/lib/Kronolith.php
kronolith/lib/View/ExportEvent.php

index fbc8985..5147b8f 100644 (file)
@@ -315,13 +315,20 @@ class Horde_iCalendar {
     /**
      * Add a vCalendar component (eg vEvent, vTimezone, etc.).
      *
-     * @param Horde_iCalendar $component  Component (subclass) to add.
+     * @param mixed  Either a Horde_iCalendar component (subclass) or an array
+     *               of them.
      */
-    function addComponent($component)
+    function addComponent($components)
     {
-        if ($component instanceOf Horde_iCalendar) {
-            $component->_container = &$this;
-            $this->_components[] = &$component;
+        if (!is_array($components)) {
+            $components = array($components);
+        }
+
+        foreach ($components as $component) {
+            if ($component instanceOf Horde_iCalendar) {
+                $component->_container = &$this;
+                $this->_components[] = $component;
+            }
         }
     }
 
index 351cbb4..b5f858b 100644 (file)
@@ -488,7 +488,7 @@ class Kronolith_Api extends Horde_Registry_Api
             $calendar = Kronolith::getDefaultCalendar();
         }
 
-        if ($calendar === false || 
+        if ($calendar === false ||
             !array_key_exists($calendar, Kronolith::listCalendars(false, Horde_Perms::READ))) {
             throw new Horde_Exception_PermissionDenied();
         }
@@ -564,7 +564,7 @@ class Kronolith_Api extends Horde_Registry_Api
         } else {
             $changes['modify'] = $uids;
         }
-        
+
         /* No way to figure out if this was an exception, so we must include all */
         $changes['delete'] = $this->listBy('delete', $start, $c, $end);
 
@@ -751,8 +751,7 @@ class Kronolith_Api extends Horde_Registry_Api
             $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
 
             // Create a new vEvent.
-            $vEvent = $event->toiCalendar($iCal);
-            $iCal->addComponent($vEvent);
+            $iCal->addComponent($event->toiCalendar($iCal));
 
             return $iCal->exportvCalendar();
 
@@ -806,8 +805,7 @@ class Kronolith_Api extends Horde_Registry_Api
 
             foreach ($events as $dayevents) {
                 foreach ($dayevents as $event) {
-                    $vEvent = $event->toiCalendar($iCal);
-                    $iCal->addComponent($vEvent);
+                    $iCal->addComponent($event->toiCalendar($iCal));
                 }
             }
 
index 096165a..a5d6075 100644 (file)
@@ -523,13 +523,13 @@ abstract class Kronolith_Event
      * @param Horde_iCalendar &$calendar  A Horde_iCalendar object that acts as
      *                                    a container.
      *
-     * @return Horde_iCalendar_vevent  The vEvent object for this event.
+     * @return array  An array of Horde_iCalendar_vevent objects for this event.
      */
     public function toiCalendar(&$calendar)
     {
         $vEvent = &Horde_iCalendar::newComponent('vevent', $calendar);
         $v1 = $calendar->getAttribute('VERSION') == '1.0';
-
+        $vEvents = array();
         if ($this->isAllDay()) {
             $vEvent->setAttribute('DTSTART', $this->start, array('VALUE' => 'DATE'));
             $vEvent->setAttribute('DTEND', $this->end, array('VALUE' => 'DATE'));
@@ -744,8 +744,41 @@ abstract class Kronolith_Event
                 $vEvent->setAttribute('RRULE', $rrule);
             }
 
-            // Exceptions.
+            // Exceptions. An exception with no replacement event is represented
+            // by EXDATE, and those with replacement events are represented by
+            // a new vEvent element. We get all known replacement events first,
+            // then remove the exceptionoriginaldate from the list of the event
+            // exceptions. Any exceptions left should represent exceptions with
+            // no replacement.
             $exceptions = $this->recurrence->getExceptions();
+            $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
+            $search = new StdClass();
+            $search->start = $this->recurrence->getRecurStart();
+            $search->end = $this->recurrence->getRecurEnd();
+            $search->baseid = $this->uid;
+            $results = $kronolith_driver->search($search);
+            foreach ($results as $days) {
+                foreach ($days as $exceptionEvent) {
+                    // Need to change the UID so it links to the original
+                    // recurring event.
+                    $exceptionEvent->uid = $this->uid;
+                    $vEventException = $exceptionEvent->toIcalendar($calendar);
+                    // This should never happen, but protect against it anyway.
+                    if (count($vEventException) > 1) {
+                        throw new Kronolith_Exception(_("Unable to parse event."));
+                    }
+                    $vEventException = array_pop($vEventException);
+                    $vEventException->setAttribute('RECURRENCE-ID', $exceptionEvent->exceptionoriginaldate->timestamp());
+                    $originaldate = $exceptionEvent->exceptionoriginaldate->format('Ymd');
+                    $key = array_search($originaldate, $exceptions);
+                    if ($key !== false) {
+                        unset($exceptions[$key]);
+                    }
+                    $vEvents[] = $vEventException;
+                }
+            }
+
+            /* The remaining exceptions represent deleted recurrences */
             foreach ($exceptions as $exception) {
                 if (!empty($exception)) {
                     list($year, $month, $mday) = sscanf($exception, '%04d%02d%02d');
@@ -761,8 +794,9 @@ abstract class Kronolith_Event
                 }
             }
         }
+        array_unshift($vEvents, $vEvent);
 
-        return $vEvent;
+        return $vEvents;
     }
 
     /**
@@ -987,7 +1021,23 @@ abstract class Kronolith_Event
                 $this->recurrence->fromRRule10($rrule);
             }
 
-            // Exceptions.
+            /* Delete all existing exceptions to this event if it already exists */
+            if (!empty($this->uid)) {
+                $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
+                $search = new StdClass();
+                $search->start = $this->recurrence->getRecurStart();
+                $search->end = $this->recurrence->getRecurEnd();
+                $search->baseid = $this->uid;
+                $results = $kronolith_driver->search($search);
+                foreach ($results as $days) {
+                    foreach ($days as $exception) {
+                        $kronolith_driver->deleteEvent($exception->id);
+                    }
+                }
+            }
+
+            // Exceptions. EXDATE represents deleted events, just add the
+            // exception, no new event is needed.
             $exdates = $vEvent->getAttributeValues('EXDATE');
             if (is_array($exdates)) {
                 foreach ($exdates as $exdate) {
@@ -1000,6 +1050,21 @@ abstract class Kronolith_Event
             }
         }
 
+        // RECURRENCE-ID indicates that this event represents an exception
+        $recurrenceid = $vEvent->getAttribute('RECURRENCE-ID');
+        if (!($recurrenceid instanceof PEAR_Error)) {
+            $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
+            $originaldt = new Horde_Date($recurrenceid);
+            $this->exceptionoriginaldate = $originaldt;
+            $this->baseid = $this->uid;
+            $this->uid = null;
+            $originalEvent = $kronolith_driver->getByUID($this->baseid);
+            $originalEvent->recurrence->addException($originaldt->format('Y'),
+                                                     $originaldt->format('m'),
+                                                     $originaldt->format('d'));
+            $originalEvent->save();
+        }
+
         $this->initialized = true;
     }
 
@@ -2774,7 +2839,7 @@ abstract class Kronolith_Event
 
     /**
      * Getter for geo data
-     * 
+     *
      * @return array  An array of lat/lng data.
      */
     public function getGeolocation()
index e855d85..5f924df 100644 (file)
@@ -2243,6 +2243,7 @@ class Kronolith
             $iCal->setAttribute('X-WR-CALNAME', Horde_String::convertCharset($share->get('name'), Horde_Nls::getCharset(), 'utf-8'));
             $vevent = $event->toiCalendar($iCal);
             if ($action == self::ITIP_CANCEL && !empty($instance)) {
+                $vevent = array_pop($vevent);
                 $vevent->setAttribute('RECURRENCE-ID', $instance, array('VALUE' => 'DATE'));
             }
             $iCal->addComponent($vevent);
index b815d0a..ffe4edb 100644 (file)
@@ -36,8 +36,7 @@ class Kronolith_View_ExportEvent {
             }
         }
 
-        $vEvent = $event->toiCalendar($iCal);
-        $iCal->addComponent($vEvent);
+        $iCal->addComponent($event->toiCalendar($iCal));
         $content = $iCal->exportvCalendar();
 
         $GLOBALS['browser']->downloadHeaders(