diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index eff85c17..0ce79025 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -368,8 +368,8 @@ class libvcalendar implements Iterator private function _to_array($ve) { $event = array( - 'uid' => strval($ve->UID), - 'title' => strval($ve->SUMMARY), + 'uid' => self::convert_string($ve->UID), + 'title' => self::convert_string($ve->SUMMARY), '_type' => $ve->name == 'VTODO' ? 'task' : 'event', // set defaults 'priority' => 0, @@ -435,7 +435,11 @@ class libvcalendar implements Iterator break; case 'EXDATE': - $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], (array)self::convert_datetime($prop)); + $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], self::convert_datetime($prop, true)); + break; + + case 'RDATE': + $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], self::convert_datetime($prop, true)); break; case 'RECURRENCE-ID': @@ -459,7 +463,7 @@ class libvcalendar implements Iterator case 'LOCATION': case 'DESCRIPTION': case 'URL': - $event[strtolower($prop->name)] = str_replace('\,', ',', $prop->value); + $event[strtolower($prop->name)] = self::convert_string($prop); break; case 'CATEGORY': @@ -671,13 +675,21 @@ class libvcalendar implements Iterator return $this->freebusy; } + /** + * + */ + public static function convert_string($prop) + { + return str_replace('\,', ',', strval($prop->value)); + } + /** * Helper method to correctly interpret an all-day date value */ - public static function convert_datetime($prop) + public static function convert_datetime($prop, $as_array = false) { if (empty($prop)) { - return null; + return $as_array ? array() : null; } else if ($prop instanceof VObject\Property\MultiDateTime) { $dt = array(); @@ -693,10 +705,38 @@ class libvcalendar implements Iterator $dt->_dateonly = true; } } + else if ($prop instanceof VObject\Property && ($prop['VALUE'] == 'DATE' || $prop['VALUE'] == 'DATE-TIME')) { + try { + list($type, $dt) = VObject\Property\DateTime::parseData($prop->value, $prop); + $dt->_dateonly = ($type & VObject\Property\DateTime::DATE); + } + catch (Exception $e) { + // ignore date parse errors + } + } + else if ($prop instanceof VObject\Property && $prop['VALUE'] == 'PERIOD') { + $dt = array(); + foreach(explode(',', $prop->value) as $val) { + try { + list($start, $end) = explode('/', $val); + list($type, $item) = VObject\Property\DateTime::parseData($start, $prop); + $item->_dateonly = ($type & VObject\Property\DateTime::DATE); + $dt[] = $item; + } + catch (Exception $e) { + // ignore single date parse errors + } + } + } else if ($prop instanceof DateTime) { $dt = $prop; } + // force return value to array if requested + if ($as_array && !is_array($dt)) { + $dt = empty($dt) ? array() : array($dt); + } + return $dt; } @@ -839,8 +879,13 @@ class libvcalendar implements Iterator if ($exdates = $event['recurrence']['EXDATE']) { unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value } + if ($rdates = $event['recurrence']['RDATE']) { + unset($event['recurrence']['RDATE']); // don't serialize RDATEs into RRULE value + } - $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'])); + if ($event['recurrence']['FREQ']) { + $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'])); + } // add EXDATEs each one per line (for Thunderbird Lightning) if ($exdates) { @@ -853,6 +898,13 @@ class libvcalendar implements Iterator } } } + // add RDATEs + if (!empty($rdates)) { + $sample = self::datetime_prop('RDATE', $rdates[0]); + $rdprop = new VObject\Property\MultiDateTime('RDATE', null); + $rdprop->setDateTimes($rdates, $sample->getDateType()); + $ve->add($rdprop); + } } if ($event['categories']) { diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index 91cb9e8f..32e3aa87 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -237,6 +237,19 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals("Me, meets Them\nThem, meet Me", $event['description'], "Decode description value"); } + /** + * Parse RDATE properties (#2885) + */ + function test_rdate() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8'); + $event = $events[0]; + + $this->assertEquals(9, count($event['recurrence']['RDATE'])); + $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]); + } + /** * @depends test_import */ @@ -388,7 +401,19 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME:20131113', $ics, "Recurrence-ID (2) being the exception date"); $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title"); } - + + /** + * + */ + function test_export_rdate() + { + $ical = new libvcalendar(); + $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8'); + $ics = $ical->export($events, null, false); + + $this->assertContains('RDATE;VALUE=DATE-TIME:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values"); + } + /** * @depends test_export */ diff --git a/plugins/libcalendaring/tests/resources/multiple-rdate.ics b/plugins/libcalendaring/tests/resources/multiple-rdate.ics index 51b938d0..a501706b 100644 --- a/plugins/libcalendaring/tests/resources/multiple-rdate.ics +++ b/plugins/libcalendaring/tests/resources/multiple-rdate.ics @@ -23,8 +23,8 @@ BEGIN:VEVENT DTSTART;TZID="W. Europe":20140520T040000 DTEND;TZID="W. Europe":20140520T200000 TRANSP:TRANSPARENT -RDATE;VALUE=PERIOD:20140520T020000Z/20140520T180000Z - ,PERIOD:20150520T020000Z/20150520T180000Z +RDATE;VALUE=DATE-TIME:20140520T020000Z +RDATE;VALUE=PERIOD:20150520T020000Z/20150520T180000Z ,20160520T020000Z/20160520T180000Z,20170520T020000Z/20170520T180000Z ,20180520T020000Z/20180520T180000Z,20190520T020000Z/20190520T180000Z ,20200520T020000Z/20200520T180000Z,20210520T020000Z/20210520T180000Z diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php index 979aeef7..4af692c4 100644 --- a/plugins/libkolab/lib/kolab_format_xcal.php +++ b/plugins/libkolab/lib/kolab_format_xcal.php @@ -203,6 +203,13 @@ abstract class kolab_format_xcal extends kolab_format } } + if ($rdates = $this->obj->recurrenceDates()) { + for ($i=0; $i < $rdates->size(); $i++) { + if ($rdate = self::php_datetime($rdates->get($i))) + $object['recurrence']['RDATE'][] = $rdate; + } + } + // read alarm $valarms = $this->obj->alarms(); $alarm_types = array_flip($this->alarm_type_map); @@ -338,7 +345,7 @@ abstract class kolab_format_xcal extends kolab_format $rr = new RecurrenceRule; $rr->setFrequency(RecurrenceRule::FreqNone); - if ($object['recurrence']) { + if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) { $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]); if ($object['recurrence']['INTERVAL']) @@ -395,6 +402,14 @@ abstract class kolab_format_xcal extends kolab_format $this->obj->setRecurrenceRule($rr); + // save recurrence dates (aka RDATE) + if (!empty($object['recurrence']['RDATE'])) { + $rdates = new vectordatetime; + foreach ((array)$object['recurrence']['RDATE'] as $rdate) + $rdates->push(self::get_datetime($rdate, null, true)); + $this->obj->setRecurrenceDates($rdates); + } + // save alarm $valarms = new vectoralarm; if ($object['alarms']) {