diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 20f9f164..5c88ed17 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -54,7 +54,18 @@ * 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as * 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest) * 'sensitivity' => 'public|private|confidential', // Event sensitivity - * 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event) + * 'alarms' => '-15M:DISPLAY', // DEPRECATED Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event) + * 'valarms' => array( // List of reminders (new format), each represented as a hash array: + * array( + * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object + * 'action' => 'DISPLAY|EMAIL|AUDIO', + * 'duration' => 'PT15M', // ISO 8601 period string + * 'repeat' => 0, // number of repetitions + * 'description' => '', // text to display for DISPLAY actions + * 'summary' => '', // message text for EMAIL actions + * 'attendees' => array(), // list of email addresses to receive alarm messages + * ), + * ), * 'attachments' => array( // List of attachments * 'name' => 'File name', * 'mimetype' => 'Content type', diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index d74db9f7..2f01e524 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -575,6 +575,7 @@ class libvcalendar implements Iterator foreach ($ve->select('VALARM') as $valarm) { $action = 'DISPLAY'; $trigger = null; + $alarm = array(); foreach ($valarm->children as $prop) { switch ($prop->name) { @@ -582,22 +583,39 @@ class libvcalendar implements Iterator foreach ($prop->parameters as $param) { if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') { $trigger = '@' . $prop->getDateTime()->format('U'); + $alarm['trigger'] = $prop->getDateTime(); } } if (!$trigger && ($values = libcalendaring::parse_alaram_value($prop->value))) { $trigger = $values[2]; + $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $prop->value), 'T'); } break; case 'ACTION': - $action = $prop->value; + $action = $alarm['action'] = strtoupper($prop->value); + break; + + case 'SUMMARY': + case 'DESCRIPTION': + case 'DURATION': + $alarm[strtolower($prop->name)] = self::convert_string($prop); + break; + + case 'REPEAT': + $alarm['repeat'] = intval($prop->value); + break; + + case 'ATTENDEE': + $alarm['attendees'][] = preg_replace('/^mailto:/i', '', $prop->value); break; } } if ($trigger && strtoupper($action) != 'NONE') { - $event['alarms'] = $trigger . ':' . $action; - break; + if (!$event['alarms']) // store first alarm in legacy property + $event['alarms'] = $trigger . ':' . $action; + $event['valarms'][] = $alarm; } } @@ -954,7 +972,37 @@ class libvcalendar implements Iterator $ve->add(self::datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true)); } - if ($event['alarms']) { + if ($event['valarms']) { + foreach ($event['valarms'] as $alarm) { + $va = VObject\Component::create('VALARM'); + $va->action = $alarm['action']; + if ($alarm['trigger'] instanceof DateTime) { + $va->add(self::datetime_prop('TRIGGER', $alarm['trigger'], true)); + } + else { + $va->add('TRIGGER', $alarm['trigger']); + } + + if ($alarm['action'] == 'EMAIL') { + foreach ((array)$alarm['attendees'] as $attendee) { + $va->add('ATTENDEE', 'mailto:' . $attendee); + } + } + if ($alarm['description']) { + $va->add('DESCRIPTION', $alarm['description'] ?: $event['title']); + } + if ($alarm['summary']) { + $va->add('SUMMARY', $alarm['summary']); + } + if ($alarm['duration']) { + $va->add('DURATION', $alarm['duration']); + $va->add('REPEAT', intval($alarm['repeat'])); + } + $ve->add($va); + } + } + // legacy support + else if ($event['alarms']) { $va = VObject\Component::create('VALARM'); list($trigger, $va->action) = explode(':', $event['alarms']); $val = libcalendaring::parse_alaram_value($trigger); diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index 0de80e6b..a25455c4 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -167,7 +167,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase } /** - * @depends test_import_from_file + * */ function test_alarms() { @@ -181,6 +181,9 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('12', $alarm[0], "Alarm value"); $this->assertEquals('-H', $alarm[1], "Alarm unit"); + $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)"); + $this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); + // alarm trigger with 0 values $events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8'); $event = $events[0]; @@ -190,6 +193,25 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('30', $alarm[0], "Alarm value"); $this->assertEquals('-M', $alarm[1], "Alarm unit"); $this->assertEquals('-30M', $alarm[2], "Alarm string"); + + $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action"); + $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text"); + + $this->assertEquals(2, count($event['valarms']), "List all VALARM blocks"); + + $valarm = $event['valarms'][1]; + $this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees"); + $this->assertEquals('EMAIL', $valarm['action'], "Second alarm item (action)"); + $this->assertEquals('-P1D', $valarm['trigger'], "Second alarm item (trigger)"); + $this->assertEquals('This is the reminder message', $valarm['summary'], "Email alarm text"); + + // test alarms export + $ics = $ical->export(array($event)); + $this->assertContains('ACTION:DISPLAY', $ics, "Display alarm block"); + $this->assertContains('ACTION:EMAIL', $ics, "Email alarm block"); + $this->assertContains('DESCRIPTION:This is the first event reminder', $ics, "Alarm description"); + $this->assertContains('SUMMARY:This is the reminder message', $ics, "Email alarm summary"); + $this->assertContains('ATTENDEE:mailto:reminder-recipient@example.org', $ics, "Email alarm recipient"); } /** @@ -222,6 +244,10 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $alarm = libcalendaring::parse_alaram_value($event['alarms']); $this->assertEquals('45', $alarm[0], "Alarm value"); $this->assertEquals('-M', $alarm[1], "Alarm unit"); + + $this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks"); + $this->assertEquals('AUDIO', $event['valarms'][0]['action'], "Full alarm item (action)"); + $this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); } /** diff --git a/plugins/libcalendaring/tests/resources/alarms.ics b/plugins/libcalendaring/tests/resources/alarms.ics index a9d7b195..d6f9d547 100644 --- a/plugins/libcalendaring/tests/resources/alarms.ics +++ b/plugins/libcalendaring/tests/resources/alarms.ics @@ -35,9 +35,17 @@ SUMMARY:Alarms test TRANSP:OPAQUE BEGIN:VALARM ACTION:DISPLAY -DESCRIPTION:This is an event reminder +DESCRIPTION:This is the first event reminder TRIGGER:-P0DT0H30M0S END:VALARM +BEGIN:VALARM +ACTION:EMAIL +DESCRIPTION:This is an event reminder +TRIGGER:-P1D +ATTENDEE:mailto:reminder-recipient@example.org +SUMMARY:This is the reminder message +DESCRIPTION:This is the second event reminder +END:VALARM END:VEVENT END:VCALENDAR diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php index 0de170d9..b244450b 100644 --- a/plugins/libkolab/lib/kolab_format_xcal.php +++ b/plugins/libkolab/lib/kolab_format_xcal.php @@ -217,27 +217,59 @@ abstract class kolab_format_xcal extends kolab_format // read alarm $valarms = $this->obj->alarms(); $alarm_types = array_flip($this->alarm_type_map); + $object['valarms'] = array(); for ($i=0; $i < $valarms->size(); $i++) { $alarm = $valarms->get($i); $type = $alarm_types[$alarm->type()]; if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported + $valarm = array( + 'action' => $type, + 'summary' => $alarm->summary(), + 'description' => $alarm->description(), + ); + + if ($type == 'EMAIL') { + $valarm['attendees'] = array(); + $attvec = $this->obj->attendees(); + for ($j=0; $j < $attvec->size(); $j++) { + $cr = $attvec->get($j); + $valarm['attendees'][] = $cr->email(); + } + } + if ($start = self::php_datetime($alarm->start())) { $object['alarms'] = '@' . $start->format('U'); + $valarm['trigger'] = $start; } else if ($offset = $alarm->relativeStart()) { - $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-'; + $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-'; + $value = $time = ''; if ($w = $offset->weeks()) $value .= $w . 'W'; else if ($d = $offset->days()) $value .= $d . 'D'; - else if ($h = $offset->hours()) $value .= $h . 'H'; - else if ($m = $offset->minutes()) $value .= $m . 'M'; - else if ($s = $offset->seconds()) $value .= $s . 'S'; + else if ($h = $offset->hours()) $time .= $h . 'H'; + else if ($m = $offset->minutes()) $time .= $m . 'M'; + else if ($s = $offset->seconds()) $time .= $s . 'S'; else continue; - $object['alarms'] = $value; + $object['alarms'] = $prefix . $value . $time; + $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : ''); } - $object['alarms'] .= ':' . $type; - break; + + // read alarm duration and repeat properties + if (($duration = $alarm->duration()) && $duration->isValid()) { + $value = $time = ''; + if ($w = $duration->weeks()) $value .= $w . 'W'; + else if ($d = $duration->days()) $value .= $d . 'D'; + else if ($h = $duration->hours()) $time .= $h . 'H'; + else if ($m = $duration->minutes()) $time .= $m . 'M'; + else if ($s = $duration->seconds()) $time .= $s . 'S'; + $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : ''); + $valarm['repeat'] = $alarm->numrepeat(); + } + + $object['alarms'] .= ':' . $type; // legacy property + $object['valarms'][] = array_filter($valarm); } } @@ -397,7 +429,60 @@ abstract class kolab_format_xcal extends kolab_format // save alarm $valarms = new vectoralarm; - if ($object['alarms']) { + if ($object['valarms']) { + foreach ($object['valarms'] as $valarm) { + if (!array_key_exists($valarm['type'], $this->alarm_type_map)) { + continue; // skip unknown alarm types + } + + if ($valarm['type'] == 'EMAIL') { + $recipients = new vectorcontactref; + foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) { + $recipients->push(new ContactReference(ContactReference::EmailReference, $email)); + } + $alarm = new Alarm( + strval($valarm['summary'] ?: $object['title']), + strval($valarm['description'] ?: $object['description']), + $recipients + ); + } + else { + $alarm = new Alarm(strval($valarm['summary'] ?: $object['title'])); + } + + if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) { + $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC'))); + } + else { + try { + $prefix = $valarm['trigger'][0]; + $period = new DateInterval(preg_replace('/[0-9PTWDHMS]/', '', $valarm['trigger'])); + $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-'); + } + catch (Exception $e) { + // skip alarm with invalid trigger values + continue; + } + + $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End); + } + + if ($valarm['duration']) { + try { + $d = new DateInterval($valarm['duration']); + $duration = new Duration($d->d, $d->h, $d->i, $d->s); + $alarm->setDuration($duration, intval($valarm['repeat'])); + } + catch (Exception $e) { + // ignore + } + } + + $valarms->push($alarm); + } + } + // legacy support + else if ($object['alarms']) { list($offset, $type) = explode(":", $object['alarms']); if ($type == 'EMAIL' && !empty($object['_owner'])) { // email alarms implicitly go to event owner