Page MenuHomePhorge

Kolab does not import ics files / appointments with non olson timezones correctly
Open, 40Public

Description

Hi,

Kolab seems to have trouble working with unknown non olson timezones in appointments. During import of such appointments either by the import function or by accepting an invitation via email with non olson timezones in any dates, these dates/time are simply imported as a numeric value in the local time zone without any conversion. These happens for instance during import from WebEX invitations.

Example:

pykolab-0.8.7-2.5.el7.kolab_16.noarch
php-sabre-vobject-3.5.3-4.1.el7.kolab_16.noarch

ICS File:

BEGIN:VCALENDAR
PRODID:-//Microsoft Corporation//Outlook 10.0 MIMEDIR//EN
VERSION:2.0
METHOD:REQUEST
BEGIN:VTIMEZONE
TZID:Eastern Time
BEGIN:STANDARD
DTSTART:20161101T020000
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:Standard Time
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20160301T020000
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:Daylight Savings Time
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;CN="";ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:jdoe@example.org
ORGANIZER;CN="name via Cisco WebEx":MAILTO:webex@example.org
DTSTART;TZID="Eastern Time":20180430T100000
DTEND;TZID="Eastern Time":20180430T110000
LOCATION:WebEX Meeting
TRANSP:OPAQUE
SEQUENCE:1524696863
UID:baed324f-61b2-4388-b85c-f8bd07b48712
DTSTAMP:20180430T140000Z
DESCRIPTION:JOIN WEBEX MEETING
SUMMARY:Test WebEX Meeting
PRIORITY:5
CLASS:PUBLIC
END:VEVENT
END:VCALENDAR

This meeting is scheduled at 10 am local timezone (Europe/Berlin in my test case). It should be scheduled at 7pm local time.

Looking into the code it seems the error happens down in the sabre-io vobject part. There seems to be no handling for VTIMEZONE objects while converting dates.

I have attached a patch for VTimeZone.php and DateTime.php to detect such errors and do a VTIMEZONE based conversion of all dates to the local timezone.

--- BUILD/sabre-vobject-3.5.3/lib/Component/VTimeZone.php	2016-10-07 05:20:40.000000000 +0200
+++ sabre-vobject-3.5.3/lib/Component/VTimeZone.php	2018-04-27 15:04:06.088342389 +0200
@@ -31,6 +31,77 @@
     }
 
     /**
+     * Returns the nearest match in a timezone rrule 
+     *
+     * @param \DateTime $dtstart
+     * @param \Sabre\VObject\Property\ICalendar\Recur	$rrule
+     * @param \DateTime $TimeToMatch
+     * @return \Sabre\VObject\Component\VEvent 
+     */
+    private function getNearestMatch( $dtstart, $rrule, $TimeToMatch) {
+      $ve = new VEvent($this->root, "STANDARD");
+      $ve->__set("DTSTART",$dtstart);
+      $ve->__set("RRULE",$rrule);
+                                        
+      $it = new \Sabre\VObject\Recur\EventIterator($ve, null, $TimeToMatch->getTimezone());
+      $tzdiff = null;
+      $return = null;
+                                                  
+      // find the last reoccurance before the given date
+      while ( $it->valid() ) {
+        $diff = $TimeToMatch->diff($it->getDtStart());
+
+        if ( !isset($tzdiff) or ($diff->invert >0) ) {
+          $tzdiff = $diff;
+          $return = $it->getEventObject();
+        } else {
+          break;
+        }
+
+        $it->next();
+      }
+    
+      return $return;                                                                                                                                                                                          
+    }
+    
+    /**
+     * Returns the UTC offset for a given time (time is threaded as given in this VTIMEZONE)
+     *  This is calculated by searching for the nearest starting point of an time switching 
+     *  event in the VTIMEZONE.
+     *  If for any reason the timezone could not be found the returned value will be 0
+     *
+     * @param \DateTime $dt
+     * @return \int
+     */
+    public function getUTCOffset(\DateTime $dt) {
+
+        $tz_offset = null;
+        $std_event  = null;
+        
+        foreach ( $this->STANDARD as $tz_standard) {
+          $tmp_event = $this->getNearestMatch( $tz_standard->DTSTART->getDateTime(), $tz_standard->RRULE, $dt);
+          if (( !isset($std_event) ) or ( $dt->diff($std_event->DTSTART->getDateTime())->days > $dt->diff($tmp_event->DTSTART->getDateTime())->days)) {
+            $std_event = $tmp_event;
+            $tz_offset = (string) $tz_standard->TZOFFSETTO;
+          }
+        }
+        
+        foreach ( $this->DAYLIGHT as $tz_daylight) {
+          $tmp_event = $this->getNearestMatch( $tz_daylight->DTSTART->getDateTime(), $tz_daylight->RRULE, $dt);
+          if ( $dt->diff($std_event->DTSTART->getDateTime())->days > $dt->diff($tmp_event->DTSTART->getDateTime())->days) {
+            $std_event = $tmp_event;
+            $tz_offset = (string) $tz_daylight->TZOFFSETTO;
+          }
+        }
+
+        if (preg_match('/(\-|\+|)([0-9][0-9])([0-9][0-9])/', $tz_offset, $matches)) 
+         return (60 * (int) $matches[3] + 3600 * (int) $matches[2]) * (int) ( $matches[1]."1" );
+        else
+         return 0;
+    }
+                                
+
+    /**
      * A simple list of validation rules.
      *
      * This is simply a list of properties, and how many times they either
--- BUILD/sabre-vobject-3.5.3/lib/Property/ICalendar/DateTime.php	2016-10-07 05:20:40.000000000 +0200
+++ sabre-vobject-3.5.3/lib/Property/ICalendar/DateTime.php	2018-04-27 15:04:06.085342378 +0200
@@ -155,6 +155,12 @@
      * property or floating time, we will use the DateTimeZone argument to
      * figure out the exact date.
      *
+     * TODO:
+     * If the timezone is not known to php, we need to convert the object 
+     * time into an olson timezone time. This will only be possible if the 
+     * object has a VTIMEZONE definition or the given object timezone is 
+     * mappable to an olson timezone.
+     *
      * @param DateTimeZone $timeZone
      * @return \DateTime[]
      */
@@ -162,19 +168,36 @@
 
         // Does the property have a TZID?
         $tzid = $this['TZID'];
-
+        $need_to_convert_to_local_timezone = false;
+                
         if ($tzid) {
-            $timeZone = TimeZoneUtil::getTimeZone((string)$tzid, $this->root);
+          try {
+            $timeZone = TimeZoneUtil::getTimeZone((string)$tzid, $this->root, true);
+          } catch(\InvalidArgumentException $e) {
+            // TimeZoneMapping failed utterly ... this means work
+
+            $need_to_convert_to_local_timezone = true;
+            $timeZone = new DateTimeZone( date_default_timezone_get() );
+          }
+            
         }
 
         $dts = array();
         foreach($this->getParts() as $part) {
-            $dts[] = DateTimeParser::parse($part, $timeZone);
+            $new_dts_value = DateTimeParser::parse($part, $timeZone);
+
+            // Do the actual converting from object timezone to local timezone
+            if ( $need_to_convert_to_local_timezone ) 
+              foreach ( $this->root->VTIMEZONE as $vtimezone) 
+                if ( strcmp( $tzid, (string) $vtimezone->TZID) == 0 ) 
+                 $new_dts_value->modify(($new_dts_value->getOffset() - $vtimezone->getUTCOffset($new_dts_value))." seconds");
+
+            $dts[] = $new_dts_value;
         }
         return $dts;
 
     }
-
+    
     /**
      * Sets the property as a DateTime object.
      *

Kind Regards,
JuWo

Details

Ticket Type
Task

Event Timeline

machniak subscribed.

Confirmed with current version.

From what I see problem is that "Eastern Time" name does not exist in mappings used by VObject. They have "Eastern Time (US & Canada)". I created a ticket to fix that https://github.com/sabre-io/vobject/issues/464. There's another ticket to improve timezone detection in VObject - https://github.com/sabre-io/vobject/issues/248. Anyway, we have to wait for upstream to fix it.