Multiple day VEVENT not working from iCal

Branjawn - October 16, 2009 - 18:02
Project:Date
Version:6.x-2.x-dev
Component:iCal API
Category:bug report
Priority:normal
Assigned:johnmunro
Status:needs review
Description

KarenS, community, I apologize for this interruption. I have spent 2 days trying to troubleshoot this and am at my wit's end.

When importing my google calendar iCal feed, everything works gloriously... except for one event which is multiple "all days".

The date start is Oct 23, 2009 12:00am
the date end is Oct 23, 2009 12:00am

...in reality the event goes until Oct 25. So, "to date" is not being imported correctly. I know this isn't a bug, I just don't know how to do it and I'm on the verge of a breakdown here feeling that I've exhausted all of my searching capabilities to find the answer.

Thanks for any insight.

#1

Branjawn - October 17, 2009 - 16:36

I have figured out that some events import with DTSTART;VALUE=DATE:20090417 and others have only DTSTART:20081122T000000Z. Could this be messing things up?

#2

Branjawn - October 17, 2009 - 16:53

More info, read this somewhere:

The default value type for DTSTART is DATE-TIME, but
20060315 is not a DATE-TIME; you can use DATE values in
DTSTART, but you have to be explicit:

DTSTART;VALUE=DATE:20060315

So, I'm trying to import the same field DTSTART as Date and Datetime? Could that be screwing things up? If so, there is nothing I can do about it right? Because the feed is generated by Google Calendar and I have no control over that...

#3

Branjawn - October 19, 2009 - 18:31
Component:Documentation» Code
Category:support request» bug report

#4

ekes - October 19, 2009 - 20:35

OK it sounds like the feed isn't doing anything wrong from what I understand of what you're saying.

Any chance you could put a copy of the feed, or point me to the feed, somewhere? I'll give a test out from there.

#5

johnmunro - October 22, 2009 - 02:07

I have a sample for you. For the following valid test ICS:

BEGIN:VCALENDAR
PRODID:Zimbra-Calendar-Provider
VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:(GMT+09.00) Osaka / Sapporo / Tokyo
BEGIN:STANDARD
DTSTART:19710101T000000
TZOFFSETTO:+0900
TZOFFSETFROM:+0900
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:FM696
SUMMARY:Herbstferien
DESCRIPTION:...
ORGANIZER;SENT-BY="mailto:wels@domain.jp":mailto:events@domain.jp
X-MS-OLK-SENDER:mailto:wels@domain.jp
DTSTART;VALUE=DATE:20091026
DTEND;VALUE=DATE:20091031
STATUS:CONFIRMED
CLASS:PUBLIC
X-MICROSOFT-CDO-ALLDAYEVENT:TRUE
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
TRANSP:OPAQUE
X-MICROSOFT-DISALLOW-COUNTER:TRUE
DTSTAMP:20090909T020325Z
SEQUENCE:50
END:VEVENT
END:VCALENDAR

As per the example given in the 'Map' tab, this gets parsed as:

Array
(
    [title] => Herbstferien
    [description] => ...
    [options] => Array
        (
            [original_author] =>
            [location] =>
            [original_url] =>
            [timestamp] => 1252461805
            [guid] => FM696
            [tags] =>
            [VEVENT] => Array
                (
                    [DATE] => Array
                        (
                            [DTSTART] => Array
                                (
                                    [datetime] => 2009-10-26
                                    [all_day] => 1
                                    [tz] =>
                                )
                            [DTEND] => Array
                                (
                                    [datetime] => 2009-10-26
                                    [all_day] => 1
                                    [tz] =>
                                )
                        )
                    [UID] => FM696
                    [SUMMARY] => Herbstferien
                    [DESCRIPTION] => ...
                    [ORGANIZER] => wels@dsty.ac.jp":mailto:events@domain.jp
                    [X-MS-OLK-SENDER] => mailto:wels@domain.jp
                    [STATUS] => CONFIRMED
                    [CLASS] => PUBLIC
                    [X-MICROSOFT-CDO-ALLDAYEVENT] => TRUE
                    [X-MICROSOFT-CDO-INTENDEDSTATUS] => BUSY
                    [TRANSP] => OPAQUE
                    [X-MICROSOFT-DISALLOW-COUNTER] => TRUE
                    [DTSTAMP] => Array
                        (
                            [datetime] => 2009-09-09 02:03:25
                            [all_day] =>
                            [tz] => UTC
                        )
                    [SEQUENCE] => 50
                    [all_day] => 1
                )
        )
)

As you can see the DTEND array is incorrect. The [datetime] seems to be coming from the DTSTART value as
[datetime] => 2009-10-26
It should be:
[datetime] => 2009-10-31

#6

johnmunro - October 22, 2009 - 03:35

Further to the above, changing the date info from:

DTSTART;VALUE=DATE:20091026
DTEND;VALUE=DATE:20091031

to:
DTSTART;VALUE=DATE:20091026T000000
DTEND;VALUE=DATE:20091031T000000

has no effect.

However changing it to:

DTSTART:20091026T000000
DTEND:20091031T000000

works.

But

DTSTART:20091026
DTEND:20091031

does not work.

We need to note that DTSTART;VALUE=DATE:20091026 is valid and that one ics file may have a variety of formats depending on the actual event and (more importantly) the client that created the event.

#7

johnmunro - October 22, 2009 - 04:18

The relevant code in parser_ical.module is:

    // Keep iCal timezone information in the feed item so we can create the right date value.
    $date_info = array('DTSTART', 'DTEND', 'RDATE', 'EXDATE', 'DURATION', 'RRULE');
    foreach ($date_info as $key) {
      if (isset($event[$key])) {
        $item->options->VEVENT['DATE'][$key] = $event[$key];
        unset($event[$key]);
      }
    }

This seems perfectly clear that the correct information is being passed back to the caller of the 'parser_ical_feedapi_feed' hook. so the misinterpretation of the DTEND must be in the date_api or feed_api module or feedapi_mapper. Correct?

#8

johnmunro - October 22, 2009 - 08:05
Project:iCal feed parser» Date
Version:6.x-1.1» 6.x-2.4

It appears the problem occurs before the code in #7 is run. The $event array contains the following at runtime:

DTSTART:
Array ( [datetime] => 2009-10-26 [all_day] => 1 [tz] => )

DTEND:
Array ( [datetime] => 2009-10-26 [all_day] => 1 [tz] => )

In fact on line 82, $feed_folded has DTEND;VALUE=DATE:20091031 [20]
whereas after

  $ical_parsed = date_ical_parse($feed_folded);

$ical_parsed has: [DTEND] => Array ( [datetime] => 2009-10-26 [all_day] => 1 [tz] => )

This narrows the bug to within the date_ical_parse function in the date_api_ical.inc file of date module so moving this bug report to the date module.

#9

johnmunro - October 22, 2009 - 08:08
Component:Code» iCal API

#10

johnmunro - October 22, 2009 - 08:37

Found it! Problem lies in the logic of the 'if' statement on line 203 of date_api_ical.inc

            if (!empty($subgroup['DTSTART']) && (!empty($subgroup['DTSTART']['all_day']) ||
            (empty($subgroup['DTEND']) && empty($subgroup['RRULE']) && empty($subgroup['RRULE']['COUNT'])))) {
              // Many programs output DTEND for an all day event as the
              // following day, reset this to the same day for internal use.
              $subgroup['all_day'] = TRUE;
              $subgroup['DTEND'] = $subgroup['DTSTART'];
            }

The IF condition is assuming 'all_day' means only one day. Bad assumption. Can someone suggest better condition code?

              // Many programs output DTEND for an all day event as the
              // following day, reset this to the same day for internal use.

#11

johnmunro - October 22, 2009 - 15:45
Title:'To date' not importing» Multiple day VEVENT not working from iCal
Version:6.x-2.4» 6.x-2.x-dev
Assigned to:Anonymous» johnmunro

I see dev still has the same code so I will do some follow up and get some better code for this case.

The case is where the iCal generator (Google calendar, Zimbra calendar, and others) produces output with DTEND one day following DTSTART. Following some research, this is correct. From http://www.ietf.org/rfc/rfc2445.txt (Page 52)

   The "DTSTART" property for a "VEVENT" specifies the inclusive start
   of the event. For recurring events, it also specifies the very first
   instance in the recurrence set. The "DTEND" property for a "VEVENT"
   calendar component specifies the non-inclusive end of the event.

My interpretation of this is that if there is no time specified then the actual end date is DTEND minus one day. How does one do this in PHP? That is:

            if (!empty($subgroup['DTSTART']) && (!empty($subgroup['DTSTART']['all_day']) ||
            (empty($subgroup['DTEND']) && empty($subgroup['RRULE']) && empty($subgroup['RRULE']['COUNT'])))) {
              // Many programs output DTEND for an all day event as the
              // following day, reset this to the same day for internal use.
              $subgroup['all_day'] = TRUE;
              if (!empty($subgroup['DTEND']) {
                $subgroup['DTEND'] = $subgroup['DTEND'] MINUS ONE DAY;
              }
              else {
                $subgroup['DTEND'] = $subgroup['DTSTART'];
              }
            }

I am a bit of a newb in PHP, sorry. How does one best do $subgroup['DTEND'] = $subgroup['DTEND'] MINUS ONE DAY; taking into account month/year ends, leap years, etc?

I have been following this for more than a year and am really happy to see we are almost there. Thanks Karen for the near perfect iCal solution you have managed on this.

#12

johnmunro - October 25, 2009 - 00:01

The logic of the above condition is:

IF a start date is defined
  AND IF  ( start date is defined as ALL_DAY )   OR   ( there is no definition for the end date )
    THEN make the end date equal the start date
         define the entire event as a single all day event

This should be:

// iCal spec states 'The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive
// end of the event'
// This is fine for hours such as 11:00~12:00 means one hour, not two but
// for days the current calendar code assumes the end date is inclusive so Oct 30 ~ Oct 31 is interpreted
// as two days, not one. So make an adjustment to account for this difference in logic
IF an end date is defined  AND  end date is defined as ALL_DAY )
    THEN make the end date one day earlier

IF a start date is defined  AND  there is no definition for the end date
    THEN make the end date equal the start date
         define the entire event as a single all day event

Karen, I would have suggested that the calendar code logic be adjusted to be the same as the iCal logic, but this may cause too many other things to break, not to mention user confusion if the granularity is set to y/m/d with no time. So the above may be the best solution?

Again, how do we make the end date one day earlier taking into account month ends, year ends, leap years, etc?

#13

johnmunro - October 25, 2009 - 04:43

Ok, thanks for the rush to help with my basic newb date question, but I believe I have found the solution as:

$subgroup['DTEND']['datetime'] = date( 'Y-m-d', date_convert($subgroup['DTEND']['datetime'], DATE_DATETIME, DATE_UNIX) - 86400);

If there is a faster or more elegant solution let me know, otherwise the solution to this bug seems to be the following code from line 198 of date_api_ical.inc replacing the single if statement with the following two:

            // iCal spec states 'The "DTEND" property for a "VEVENT" calendar component specifies the
            // non-inclusive end of the event'
            // This is fine for hours such as 11:00~12:00 means one hour, not two but
            // for whole days the current calendar code assumes the end date is inclusive so Oct 30 ~ Oct 31 is
            // interpreted as two days, not one. So make an adjustment to account for this difference in logic
            if (!empty($subgroup['DTEND']) && (!empty($subgroup['DTEND']['all_day']))) {
              // make the end date one day earlier
              $subgroup['DTEND']['datetime'] = date( 'Y-m-d', date_convert($subgroup['DTEND']['datetime'], DATE_DATETIME, DATE_UNIX) - 86400);
            }
            // Add a top-level indication for the 'All day' condition.
            // Leave it in the individual date components, too, so it
            // is always available even when you are working with only
            // a portion of the VEVENT array, like in Feed API parsers.
            $subgroup['all_day'] = FALSE;
            // if a start datetime is defined  AND  there is no definition for the end datetime
            //  THEN make the end datetime equal the start datetime
            //  and if it is an all day event define the entire event as a single all day event
            if (!empty($subgroup['DTSTART']) &&
               (empty($subgroup['DTEND']) && empty($subgroup['RRULE']) && empty($subgroup['RRULE']['COUNT']))) {
              $subgroup['DTEND'] = $subgroup['DTSTART'];
              if (!empty($subgroup['DTSTART']['all_day'])) {
                $subgroup['all_day'] = TRUE;
              }
            }

I am not able to know the purpose of $subgroup['all_day'] = TRUE; and whether it should also be applied to the multi-day events or not for the benefit of Calendar, but the above code works for me.

Can someone else please test this too?

#14

Branjawn - November 2, 2009 - 17:24

Thanks for all the work being done here to correct the problem.

#15

johnmunro - November 3, 2009 - 00:09

In a highly related issue the following code needs to be inserted in theme/theme.inc at line 385 just after   $end = date_format($end_date, DATE_FORMAT_DATETIME);

  if (substr($end, -8) == "00:00:00") {
    // it is an all day event so make the end date one day later to satisfy the 'Ongoing' condition
    $end = date_convert(date_convert($end, DATE_DATETIME, DATE_UNIX) + 86400, DATE_UNIX, DATE_DATETIME);
  }

This is not just an iCal issue, but is relevant due to the unbalanced logic of the end date being inclusive for all day events verses non-inclusive for everything else (hours, minutes, seconds)

@Branjawn
Can you try the code from #13 along with this. If it works for you, I'll find out how to roll a patch for Karen's convenience.

#16

johnmunro - November 11, 2009 - 01:58

I now have this working well on a production site so attaching patches for the above code for committing.

AttachmentSize
date_api_ical.patch 2.6 KB
theme.patch 716 bytes

#17

johnmunro - November 11, 2009 - 01:59
Status:active» needs review

#18

Branjawn - November 13, 2009 - 18:25

So, I totally bailed on using Date, Calendar, FeedAPI, and iCal.
There are so many problems with this right now and I spent weeks troubleshooting. I work on a shared server and cron was giving me hell because of iCal in particular. the next site I have to import a Google Calendar I'll give it another go. I'm no programmer or I would help fix the problems. I respect those of you who can.

 
 

Drupal is a registered trademark of Dries Buytaert.