0011050: VEVENT converter: fix timezone handling for all day events
authorPhilipp Schüle <p.schuele@metaways.de>
Wed, 13 May 2015 15:50:09 +0000 (17:50 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 21 May 2015 10:31:56 +0000 (12:31 +0200)
* ics has no timezone information
* we need to use user timezone in this case to fix exdate matching

https://forge.tine20.org/view.php?id=11050

Change-Id: I37389487062b3f008d106b499dc64ecfe89e7efa
Reviewed-on: http://gerrit.tine20.com/customers/1932
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
tests/tine20/Calendar/Controller/EventTests.php
tests/tine20/Calendar/Convert/Event/VCalendar/MacOSXTest.php
tests/tine20/Calendar/Import/files/apple_calendar_birthday.ics [new file with mode: 0644]
tests/tine20/Calendar/Import/files/apple_calendar_birthday2.ics [new file with mode: 0644]
tine20/Calendar/Controller/Event.php
tine20/Calendar/Convert/Event/VCalendar/Abstract.php

index 5263be9..3cfb684 100644 (file)
@@ -391,7 +391,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $this->assertFalse(in_array($persistentEvent->getId(), $events->getId()), 'event in deleted (display) container shuld not be found');
     }
     
-    public function testCreateEventWithConfict()
+    public function testCreateEventWithConflict()
     {
         $this->_testNeedsTransaction();
         
@@ -424,7 +424,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         return $persitentConflictEvent;
     }
     
-    public function testCreateEventWithConfictFromGroupMember()
+    public function testCreateEventWithConflictFromGroupMember()
     {
         $event = $this->_getEvent();
         $event->attendee = $this->_getAttendee();
@@ -496,7 +496,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
     
     public function testUpdateWithConflictNoTimechange()
     {
-        $persitentConflictEvent = $this->testCreateEventWithConfict();
+        $persitentConflictEvent = $this->testCreateEventWithConflict();
         $persitentConflictEvent->summary = 'only time updates should recheck free/busy';
         
         $this->_controller->update($persitentConflictEvent, TRUE);
@@ -504,7 +504,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
     
     public function testUpdateWithConflictAttendeeChange()
     {
-        $persitentConflictEvent = $this->testCreateEventWithConfict();
+        $persitentConflictEvent = $this->testCreateEventWithConflict();
         $persitentConflictEvent->summary = 'attendee adds should recheck free/busy';
         
         $defaultUserGroup = Tinebase_Group::getInstance()->getDefaultGroup();
@@ -520,7 +520,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
     
     public function testUpdateWithConflictWithTimechange()
     {
-        $persitentConflictEvent = $this->testCreateEventWithConfict();
+        $persitentConflictEvent = $this->testCreateEventWithConflict();
         $persitentConflictEvent->summary = 'time updates should recheck free/busy';
         $persitentConflictEvent->dtend->addHour(1);
         
@@ -1357,11 +1357,9 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         ));
     
         try {
-            $cf = Tinebase_CustomField::getInstance()->addCustomField($cfData);
+            Tinebase_CustomField::getInstance()->addCustomField($cfData);
         } catch (Zend_Db_Statement_Exception $zdse) {
-            // customfield already exists
-            $cfs = Tinebase_CustomField::getInstance()->getCustomFieldsForApplication('Calendar');
-            $cf = $cfs->filter('name', $name)->getFirstRecord();
+            // custom field already exists
         }
     
         $event = new Calendar_Model_Event(array(
index 5f4c0d6..bc34e77 100644 (file)
@@ -4,40 +4,23 @@
  * 
  * @package     Calendar
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2011-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2011-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
 /**
- * Test helper
- */
-require_once dirname(dirname(dirname(dirname(dirname(__FILE__))))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-
-/**
  * Test class for Calendar_Convert_Event_VCalendar_MacOSX
  */
-class Calendar_Convert_Event_VCalendar_MacOSXTest extends PHPUnit_Framework_TestCase
+class Calendar_Convert_Event_VCalendar_MacOSXTest extends Calendar_TestCase
 {
     /**
      * @var array test objects
      */
     protected $objects = array();
-    
-    /**
-     * Runs the test methods of this class.
-     *
-     * @access public
-     * @static
-     */
-    public static function main()
-    {
-        $suite  = new PHPUnit_Framework_TestSuite('Tine 2.0 Calendar WebDAV MacOSX Event Tests');
-        PHPUnit_TextUI_TestRunner::run($suite);
-    }
-    
+
     /**
      * testBackslashInDescription
-     * 
+     *
      * @see 0009176: iCal adds another backslash to description field
      */
     public function testBackslashInDescription()
@@ -52,12 +35,12 @@ class Calendar_Convert_Event_VCalendar_MacOSXTest extends PHPUnit_Framework_Test
             'uid' => Tinebase_Record_Abstract::generateUID(),
             'seq' => 1,
         ));
-        
+
         $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_MACOSX);
         $vevent = $converter->fromTine20Model($event)->serialize();
-        
+
         $convertedEvent = $converter->toTine20Model($vevent);
-        
+
         $this->assertEquals($event->description, $convertedEvent->description);
     }
 
@@ -77,9 +60,9 @@ class Calendar_Convert_Event_VCalendar_MacOSXTest extends PHPUnit_Framework_Test
         $this->assertNotEquals($event->organizer, $event->attendee[0]->user_id, 'organizer should not attend');
 
         // assert alarm
-        $this->assertEquals(1,$event->alarms->count(), 'there should be exactly one alarm');
+        $this->assertEquals(1, $event->alarms->count(), 'there should be exactly one alarm');
         $this->assertFalse((bool)$event->alarms->getFirstRecord()->getOption('custom'), 'alarm should be duration alarm');
-        $this->assertEquals(15, $event->alarms->getFirstRecord()->minutes_before, 'alarm shoud be 15 min. before');
+        $this->assertEquals(15, $event->alarms->getFirstRecord()->minutes_before, 'alarm should be 15 min. before');
         $this->assertEquals('2013-11-15 11:47:23', Calendar_Controller_Alarm::getAcknowledgeTime($event->alarms->getFirstRecord())->format(Tinebase_Record_Abstract::ISO8601LONG), 'ACKNOWLEDGED was not imported properly');
     }
 
@@ -108,4 +91,29 @@ class Calendar_Convert_Event_VCalendar_MacOSXTest extends PHPUnit_Framework_Test
         $event = $converter->toTine20Model($vcalendarStream);
         $this->assertEquals(Calendar_Model_Event::CLASS_PRIVATE, $event->class);
     }
+
+    /**
+     * testConvertAllDayEventWithExdate
+     *
+     * - exdate is the base event
+     */
+    public function testConvertAllDayEventWithExdate()
+    {
+        $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_MACOSX, '10.10.2');
+
+        $vcalendarStream = fopen(dirname(__FILE__) . '/../../../Import/files/apple_calendar_birthday.ics', 'r');
+        $updateEvent = $converter->toTine20Model($vcalendarStream);
+        $eventWithExdateOnBaseEvent = Calendar_Controller_MSEventFacade::getInstance()->create($updateEvent);
+
+        $this->assertEquals(1, count($eventWithExdateOnBaseEvent->exdate));
+
+        // refetch existing event here and pass it to converter
+        $eventWithExdateOnBaseEvent = Calendar_Controller_Event::getInstance()->get($eventWithExdateOnBaseEvent->getId());
+
+        $vcalendarStream2 = fopen(dirname(__FILE__) . '/../../../Import/files/apple_calendar_birthday2.ics', 'r');
+        $updateEvent2 = $converter->toTine20Model($vcalendarStream2, $eventWithExdateOnBaseEvent);
+
+        $this->assertEquals(1, count($updateEvent2->exdate), print_r($updateEvent2->toArray(), true));
+        $this->assertEquals('2015-04-27 21:59:59', $updateEvent2->rrule_until);
+    }
 }
diff --git a/tests/tine20/Calendar/Import/files/apple_calendar_birthday.ics b/tests/tine20/Calendar/Import/files/apple_calendar_birthday.ics
new file mode 100644 (file)
index 0000000..80e3847
--- /dev/null
@@ -0,0 +1,40 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.4//EN
+X-CALENDARSERVER-ACCESS:PUBLIC
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20140429
+DTEND;VALUE=DATE:20140430
+TRANSP:TRANSPARENT
+X-CALENDARSERVER-ACCESS:PUBLIC
+UID:045CA4C5-2952-4070-BBDD-C835813E73A9
+DTSTAMP:20150512T151422Z
+DESCRIPTION:summary
+STATUS:CONFIRMED
+SEQUENCE:8
+CLASS:PUBLIC
+SUMMARY:Geburtstage
+LAST-MODIFIED:20150513T151448Z
+CREATED:20140823T100053Z
+RRULE:FREQ=YEARLY;UNTIL=20150428;BYMONTH=4;BYMONTHDAY=29
+END:VEVENT
+BEGIN:VEVENT
+TRANSP:TRANSPARENT
+DTEND;VALUE=DATE:20140430
+X-CALENDARSERVER-ACCESS:PUBLIC
+UID:045CA4C5-2952-4070-BBDD-C835813E73A9
+DTSTAMP:20150513T151539Z
+DESCRIPTION:
+STATUS:CONFIRMED
+SEQUENCE:3
+CLASS:PUBLIC
+RECURRENCE-ID;VALUE=DATE:20140429
+SUMMARY:Geburtstag
+X-TINE20-CONTAINER:149
+LAST-MODIFIED:20150410T130420Z
+CREATED:20140823T100054Z
+DTSTART;VALUE=DATE:20140429
+END:VEVENT
+END:VCALENDAR
+
diff --git a/tests/tine20/Calendar/Import/files/apple_calendar_birthday2.ics b/tests/tine20/Calendar/Import/files/apple_calendar_birthday2.ics
new file mode 100644 (file)
index 0000000..f73edc0
--- /dev/null
@@ -0,0 +1,41 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.4//EN
+X-CALENDARSERVER-ACCESS:PUBLIC
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20140429
+DTEND;VALUE=DATE:20140430
+TRANSP:TRANSPARENT
+X-CALENDARSERVER-ACCESS:PUBLIC
+UID:045CA4C5-2952-4070-BBDD-C835813E73A9
+DTSTAMP:20150512T151422Z
+DESCRIPTION:abc
+STATUS:CONFIRMED
+SEQUENCE:9
+CLASS:PUBLIC
+SUMMARY:Geburtstag
+X-TINE20-CONTAINER:149
+LAST-MODIFIED:20150513T151448Z
+CREATED:20140823T100053Z
+RRULE:FREQ=YEARLY;UNTIL=20150428;BYMONTH=4;BYMONTHDAY=29
+END:VEVENT
+BEGIN:VEVENT
+TRANSP:TRANSPARENT
+DTEND;VALUE=DATE:20140430
+X-CALENDARSERVER-ACCESS:PUBLIC
+UID:045CA4C5-2952-4070-BBDD-C835813E73A9
+DTSTAMP:20150513T151539Z
+DESCRIPTION:summary
+STATUS:CONFIRMED
+SEQUENCE:4
+CLASS:PUBLIC
+RECURRENCE-ID;VALUE=DATE:20140429
+SUMMARY:Geburtstage!!!
+X-TINE20-CONTAINER:149
+LAST-MODIFIED:20150410T130420Z
+CREATED:20140823T100054Z
+DTSTART;VALUE=DATE:20140429
+END:VEVENT
+END:VCALENDAR
+
index da39029..c4cf83d 100644 (file)
@@ -1285,6 +1285,10 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
 
         // compute remaining exdates
         $deletedInstanceDtStarts = array_diff(array_unique((array) $baseEvent->exdate), $exceptions->getOriginalDtStart());
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+            ' Faking ' . count($deletedInstanceDtStarts) . ' deleted exceptions');
+
         foreach((array) $deletedInstanceDtStarts as $deletedInstanceDtStart) {
             $fakeEvent = clone $baseEvent;
             $fakeEvent->setId(NULL);
index a153a03..67684a7 100644 (file)
@@ -411,8 +411,6 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
             $this->_convertVevent($baseVevent, $event, $options);
         }
         
-        // TODO only do this for events with rrule?
-        // if (! empty($event->rrule)) {
         $this->_parseEventExceptions($event, $vcalendar, $baseVevent, $options);
         
         $event->isValid(true);
@@ -453,7 +451,7 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
         if (! $event->exdate instanceof Tinebase_Record_RecordSet) {
             $event->exdate = new Tinebase_Record_RecordSet('Calendar_Model_Event');
         }
-        $recurExceptions =  $event->exdate->filter('is_deleted', false);
+        $recurExceptions = $event->exdate->filter('is_deleted', false);
         
         foreach ($vcalendar->VEVENT as $vevent) {
             if (isset($vevent->{'RECURRENCE-ID'}) && $event->uid == $vevent->UID) {
@@ -475,7 +473,7 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
                 }
                 
                 // initialize attachments from base event as clients may skip parameters like
-                // name and contentytpe and we can't backward relove them from managedId
+                // name and content type and we can't backward relove them from managedId
                 if ($event->attachments instanceof Tinebase_Record_RecordSet && 
                         ! $recurException->attachments instanceof Tinebase_Record_RecordSet) {
                     $recurException->attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
@@ -584,10 +582,10 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
      */
     protected function _getRecurException(Tinebase_Record_RecordSet $oldExdates,Sabre\VObject\Component\VEvent $vevent)
     {
-        $exDate = clone $vevent->{'RECURRENCE-ID'}->getDateTime();
-        $exDate->setTimeZone(new DateTimeZone('UTC'));
+        // we need to use the user timezone here if this is a DATE (like this: RECURRENCE-ID;VALUE=DATE:20140429)
+        $exDate = Calendar_Convert_Event_VCalendar_Abstract::getUTCDateFromStringInUsertime((string) $vevent->{'RECURRENCE-ID'});
         $exDateString = $exDate->format('Y-m-d H:i:s');
-        
+
         foreach ($oldExdates as $id => $oldExdate) {
             if ($exDateString == substr((string) $oldExdate->recurid, -19)) {
                 return $oldExdate;
@@ -843,13 +841,7 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
                     
                     // convert date format
                     $rruleString = preg_replace_callback('/UNTIL=([\dTZ]+)(?=;?)/', function($matches) {
-                        if (strlen($matches[1]) < 10) {
-                            $dtUntil = date_create($matches[1], new DateTimeZone ((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE)));
-                            $dtUntil->setTimezone(new DateTimeZone('UTC'));
-                        } else {
-                            $dtUntil = date_create($matches[1]);
-                        }
-                        
+                        $dtUntil = Calendar_Convert_Event_VCalendar_Abstract::getUTCDateFromStringInUsertime($matches[1]);
                         return 'UNTIL=' . $dtUntil->format(Tinebase_Record_Abstract::ISO8601LONG);
                     }, $rruleString);
 
@@ -1075,4 +1067,22 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
         // convert all datetime fields to UTC
         $event->setTimezone('UTC');
     }
+    
+    /**
+     * get utc datetime from date string and handle dates (ie 20140922) in usertime
+     * 
+     * @param string $dateString
+     * 
+     * TODO maybe this can be replaced with _convertToTinebaseDateTime
+     */
+    static public function getUTCDateFromStringInUsertime($dateString)
+    {
+        if (strlen($dateString) < 10) {
+            $date = date_create($dateString, new DateTimeZone ((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE)));
+        } else {
+            $date = date_create($dateString);
+        }
+        $date->setTimezone(new DateTimeZone('UTC'));
+        return $date;
+    }
 }