Commit 428cfc85 authored by Robert Rollins's avatar Robert Rollins
Browse files

Issue #2207173: RRULEs with non-UTC UNTIL values are now supported.

According to the iCal spec, the UNTIL value of an RRULE must be specified in
UTC. But certain iCal feed creation tools don't respect this rule, and
instead specify the UNTIL value in local time. Since all the other code that
Date iCal utilizes to parse RRULEs expects the UNTIL value to be in UTC,
this go badly when it's not.

In order to get around this issue, I've added an option to the iCal Parser
settings, which allows users to tell Date iCal that they expect the UNTIL
values in their feeds' RRULEs to not be in UTC. With that option enabled,
Date iCal will treat the UNTIL date as being in the same timezone as the
event's DTSTART.
parent 6e96f984
......@@ -42,17 +42,33 @@ function _date_ical_get_repeat_dates($field_name, $repeat_data, $item, $source)
$start_datetime = date_format($start, DATE_FORMAT_DATETIME);
if (!empty($rrule_values['UNTIL']['datetime'])) {
// TODO: The spec says that UNTIL must be specified in UTC, but I think
// this may not be followd by all feed creators. It may be a good idea to
// support optionally assuming the date has the same TZID as DTSTART.
$end = date_ical_date($rrule_values['UNTIL'], $timezone);
$end_datetime = date_format($end, DATE_FORMAT_DATETIME);
// The spec says that UNTIL must be in UTC, but not all feed creators
// follow that rule. If the user specified that he wanted to overcome this
// problem, use $timezone for the $final_repeat, and then convert the UNTIL
// in the unparsed RRULE to UTC.
if (!empty($source->importer->config['parser']['config']['until_not_utc'])) {
// Change the parsed UNTIL from UTC to $timezone, so that the
// date_ical_date() won't convert it.
$rrule_values['UNTIL']['tz'] = $timezone;
// Convert the unparsed UNTIL to UTC, since the Date code will use it.
// It's currently got a Z on it, but only because iCalcreator blindly
// added it. Since the user set "until_not_utc", we know it's not UTC.
$matches = array();
preg_match('/^(.*?)UNTIL=(.*?)Z(.*?)$/', $repeat_data['RRULE'], $matches);
$until_date = new DateObject($matches[2], $timezone);
$until_date->setTimezone(new DateTimeZone('UTC'));
$matches[2] = $until_date->format('Ymd\THis');
$repeat_data['RRULE'] = "{$matches[1]}UNTIL={$matches[2]}Z{$matches[3]}";
$final_repeat = date_ical_date($rrule_values['UNTIL'], $timezone);
$final_repeat_datetime = date_format($final_repeat, DATE_FORMAT_DATETIME);
elseif (!empty($rrule_values['COUNT'])) {
$end_datetime = NULL;
$final_repeat_datetime = NULL;
else {
// No UNTIL and no COUNT?
// No UNTIL and no COUNT? This is an illegal RRULE.
return array();
......@@ -69,10 +85,11 @@ function _date_ical_get_repeat_dates($field_name, $repeat_data, $item, $source)
$date = date_ical_date($rdate, $timezone);
$additions[] = date_format($date, 'Y-m-d');
$date_repeat_compatible_rrule = "{$repeat_data['RRULE']}\n{$repeat_data['RDATE']}\n{$repeat_data['EXDATE']}";
$calculated_dates = date_repeat_calc($date_repeat_compatible_rrule, $start_datetime, $end_datetime, $exceptions, $timezone, $additions);
$calculated_dates = date_repeat_calc($date_repeat_compatible_rrule, $start_datetime, $final_repeat_datetime, $exceptions, $timezone, $additions);
$repeat_dates = array();
foreach ($calculated_dates as $delta => $date) {
// date_repeat_calc always returns DATE_DATETIME dates, which is
......@@ -114,7 +131,7 @@ function _date_ical_parse_repeat_rule($repeat_rule_string) {
// These keys never have multiple values.
// The following keys never have multiple values.
if (in_array($key, array('UNTIL', 'FREQ', 'INTERVAL', 'COUNT', 'WKST'))) {
if ($key == 'UNTIL') {
// This is a function from the date_api module, not date_ical.
......@@ -64,7 +64,7 @@ class DateiCalFeedsParser extends FeedsParser {
// the subsequent batch starts on the first unparsed component.
$state->pointer = $parser->getLastComponentParsed() + 1;
$state->progress($state->total, $state->pointer);
return new FeedsParserResult($rows);
......@@ -74,6 +74,7 @@ class DateiCalFeedsParser extends FeedsParser {
public function sourceDefaults() {
return array(
'indefinite_count' => $this->config['indefinite_count'],
'until_not_utc' => $this->config['until_not_utc'],
......@@ -83,6 +84,7 @@ class DateiCalFeedsParser extends FeedsParser {
public function configDefaults() {
return array(
'indefinite_count' => '52',
'until_not_utc' => FALSE,
......@@ -103,6 +105,14 @@ class DateiCalFeedsParser extends FeedsParser {
'#description' => t('Indefinitely repeating events are not supported. The repeat count will instead be set to this number.'),
'#default_value' => $this->config['indefinite_count'],
$form['until_not_utc'] = array(
'#title' => t('RRULE UNTILs are not in UTC'),
'#type' => 'checkbox',
'#description' => t('Enable this setting if you\'re seeing reccuring events lose or gain repeat(s) at the end. ' .
'The iCal spec requires that the UNTIL value in an RRULE be specified in UTC, but some iCal feed creators fail to do this ' .
'(the UNTIL values in the feed\'s RRULEs don\'t end is "Z"). This causes the UNTIL value to be off by several hours.'),
'#default_value' => $this->config['until_not_utc'],
return $form;
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment