2 commits - plugins/calendar plugins/libcalendaring
Thomas Brüderli
bruederli at kolabsys.com
Mon Sep 22 11:36:09 CEST 2014
plugins/calendar/calendar_ui.js | 4
plugins/calendar/localization/en_US.inc | 4
plugins/libcalendaring/libvcalendar.php | 151 +++++++++++++++++++++++---
plugins/libcalendaring/tests/libvcalendar.php | 61 +++++++++-
4 files changed, 193 insertions(+), 27 deletions(-)
New commits:
commit f9b19b9f279178257a1a98511be5ecd360cede01
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Sep 22 11:03:11 2014 +0200
Include VTIMEZONE definitions when exporting event data or invitations as iCal (#3199)
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 3cb7826..939cc76 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -50,12 +50,14 @@ class libvcalendar implements Iterator
private $forward_exceptions;
private $vhead;
private $fp;
+ private $vtimezones = array();
public $method;
public $agent = '';
public $objects = array();
public $freebusy = array();
+
/**
* Default constructor
*/
@@ -106,6 +108,7 @@ class libvcalendar implements Iterator
$this->method = '';
$this->objects = array();
$this->freebusy = array();
+ $this->vtimezones = array();
$this->iteratorkey = 0;
if ($this->fp) {
@@ -791,13 +794,26 @@ class libvcalendar implements Iterator
* @param string Property name
* @param object DateTime
*/
- public static function datetime_prop($name, $dt, $utc = false, $dateonly = null)
+ public function datetime_prop($name, $dt, $utc = false, $dateonly = null)
{
$is_utc = $utc || (($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')));
$is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly;
$vdt = new VObject\Property\DateTime($name);
$vdt->setDateTime($dt, $is_dateonly ? VObject\Property\DateTime::DATE :
($is_utc ? VObject\Property\DateTime::UTC : VObject\Property\DateTime::LOCALTZ));
+
+ // register timezone for VTIMEZONE block
+ if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) {
+ $ts = $dt->format('U');
+ if (is_array($this->vtimezones[$tzname])) {
+ $this->vtimezones[$tzname][0] = min($this->vtimezones[$tzname][0], $ts);
+ $this->vtimezones[$tzname][1] = max($this->vtimezones[$tzname][1], $ts);
+ }
+ else {
+ $this->vtimezones[$tzname] = array($ts, $ts);
+ }
+ }
+
return $vdt;
}
@@ -834,9 +850,10 @@ class libvcalendar implements Iterator
* @param string VCalendar method to advertise
* @param boolean Directly send data to stdout instead of returning
* @param callable Callback function to fetch attachment contents, false if no attachment export
+ * @param boolean Add VTIMEZONE block with timezone definitions for the included events
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
*/
- public function export($objects, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
+ public function export($objects, $method = null, $write = false, $get_attachment = false, $with_timezones = true)
{
$memory_limit = parse_bytes(ini_get('memory_limit'));
$this->method = $method;
@@ -851,8 +868,6 @@ class libvcalendar implements Iterator
$vcal->METHOD = $method;
}
- // TODO: include timezone information
-
// write vcalendar header
if ($write) {
echo preg_replace('/END:VCALENDAR[\r\n]*$/m', '', $vcal->serialize());
@@ -862,6 +877,23 @@ class libvcalendar implements Iterator
$this->_to_ical($object, !$write?$vcal:false, $get_attachment);
}
+ // include timezone information
+ if ($with_timezones || !empty($method)) {
+ foreach ($this->vtimezones as $tzid => $range) {
+ $vt = self::get_vtimezone($tzid, $range[0], $range[1]);
+ if (empty($vt)) {
+ continue; // no timezone information found
+ }
+
+ if ($vcal) {
+ $vcal->add($vt);
+ }
+ else {
+ echo $vt->serialize();
+ }
+ }
+ }
+
if ($write) {
echo "END:VCALENDAR\r\n";
return true;
@@ -887,7 +919,7 @@ class libvcalendar implements Iterator
// set DTSTAMP according to RFC 5545, 3.8.7.2.
$dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime();
- $ve->add(self::datetime_prop('DTSTAMP', $dtstamp, true));
+ $ve->add($this->datetime_prop('DTSTAMP', $dtstamp, true));
// all-day events end the next day
if ($event['allday'] && !empty($event['end'])) {
@@ -896,15 +928,15 @@ class libvcalendar implements Iterator
$event['end']->_dateonly = true;
}
if (!empty($event['created']))
- $ve->add(self::datetime_prop('CREATED', $event['created'], true));
+ $ve->add($this->datetime_prop('CREATED', $event['created'], true));
if (!empty($event['changed']))
- $ve->add(self::datetime_prop('LAST-MODIFIED', $event['changed'], true));
+ $ve->add($this->datetime_prop('LAST-MODIFIED', $event['changed'], true));
if (!empty($event['start']))
- $ve->add(self::datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
+ $ve->add($this->datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
if (!empty($event['end']))
- $ve->add(self::datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
+ $ve->add($this->datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
if (!empty($event['due']))
- $ve->add(self::datetime_prop('DUE', $event['due'], false));
+ $ve->add($this->datetime_prop('DUE', $event['due'], false));
if ($recurrence_id)
$ve->add($recurrence_id);
@@ -944,7 +976,7 @@ class libvcalendar implements Iterator
}
// add RDATEs
if (!empty($rdates)) {
- $sample = self::datetime_prop('RDATE', $rdates[0]);
+ $sample = $this->datetime_prop('RDATE', $rdates[0]);
$rdprop = new VObject\Property\MultiDateTime('RDATE', null);
$rdprop->setDateTimes($rdates, $sample->getDateType());
$ve->add($rdprop);
@@ -985,7 +1017,7 @@ class libvcalendar implements Iterator
$ve->add('PERCENT-COMPLETE', intval($event['complete']));
// Apple iCal required the COMPLETED date to be set in order to consider a task complete
if ($event['complete'] == 100)
- $ve->add(self::datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
+ $ve->add($this->datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
}
if ($event['valarms']) {
@@ -993,7 +1025,7 @@ class libvcalendar implements Iterator
$va = VObject\Component::create('VALARM');
$va->action = $alarm['action'];
if ($alarm['trigger'] instanceof DateTime) {
- $va->add(self::datetime_prop('TRIGGER', $alarm['trigger'], true));
+ $va->add($this->datetime_prop('TRIGGER', $alarm['trigger'], true));
}
else {
$va->add('TRIGGER', $alarm['trigger']);
@@ -1028,7 +1060,7 @@ class libvcalendar implements Iterator
if ($val[3])
$va->add('TRIGGER', $val[3]);
else if ($val[0] instanceof DateTime)
- $va->add(self::datetime_prop('TRIGGER', $val[0]));
+ $va->add($this->datetime_prop('TRIGGER', $val[0]));
$ve->add($va);
}
@@ -1111,7 +1143,7 @@ class libvcalendar implements Iterator
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
$exdate = clone $event['start'];
$exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
- $recurrence_id = self::datetime_prop('RECURRENCE-ID', $exdate, true);
+ $recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, true);
// if ($ex['thisandfuture']) // not supported by any client :-(
// $recurrence_id->add('RANGE', 'THISANDFUTURE');
$this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
@@ -1119,6 +1151,95 @@ class libvcalendar implements Iterator
}
}
+ /**
+ * Returns a VTIMEZONE component for a Olson timezone identifier
+ * with daylight transitions covering the given date range.
+ *
+ * @param string Timezone ID as used in PHP's Date functions
+ * @param integer Unix timestamp with first date/time in this timezone
+ * @param integer Unix timestap with last date/time in this timezone
+ *
+ * @return mixed A Sabre\VObject\Component object representing a VTIMEZONE definition
+ * or false if no timezone information is available
+ */
+ public static function get_vtimezone($tzid, $from = 0, $to = 0)
+ {
+ if (!$from) $from = time();
+ if (!$to) $to = $from;
+
+ if (is_string($tzid)) {
+ try {
+ $tz = new \DateTimeZone($tzid);
+ }
+ catch (\Exception $e) {
+ return false;
+ }
+ }
+ else if (is_a($tzid, '\\DateTimeZone')) {
+ $tz = $tzid;
+ }
+
+ if (!is_a($tz, '\\DateTimeZone')) {
+ return false;
+ }
+
+ $year = 86400 * 360;
+ $transitions = $tz->getTransitions($from - $year, $to + $year);
+
+ $vt = new VObject\Component('VTIMEZONE');
+ $vt->TZID = $tz->getName();
+
+ $std = null; $dst = null;
+ foreach ($transitions as $i => $trans) {
+ $cmp = null;
+
+ if ($i == 0) {
+ $tzfrom = $trans['offset'] / 3600;
+ continue;
+ }
+
+ if ($trans['isdst']) {
+ $t_dst = $trans['ts'];
+ $dst = new VObject\Component('DAYLIGHT');
+ $cmp = $dst;
+ }
+ else {
+ $t_std = $trans['ts'];
+ $std = new VObject\Component('STANDARD');
+ $cmp = $std;
+ }
+
+ if ($cmp) {
+ $dt = new DateTime($trans['time']);
+ $offset = $trans['offset'] / 3600;
+
+ $cmp->DTSTART = $dt->format('Ymd\THis');
+ $cmp->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), 0);
+ $cmp->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '', floor($offset), 0);
+
+ if (!empty($trans['abbr'])) {
+ $cmp->TZNAME = $trans['abbr'];
+ }
+
+ $tzfrom = $offset;
+ $vt->add($cmp);
+ }
+
+ // we covered the entire date range
+ if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
+ break;
+ }
+ }
+
+ // add X-MICROSOFT-CDO-TZID if available
+ $microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
+ if (array_key_exists($tz->getName(), $microsoftExchangeMap)) {
+ $vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
+ }
+
+ return $vt;
+ }
+
/*** Implement PHP 5 Iterator interface to make foreach work ***/
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index aa2c568..d3a0ffe 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -327,7 +327,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
/**
* Test for iCal export from internal hash array representation
*
- * @depends test_extended
+ *
*/
function test_export()
{
@@ -343,12 +343,20 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
$event['attachments'][0]['id'] = '1';
$event['description'] = '*Exported by libvcalendar*';
- $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'));
+ $event['start']->setTimezone(new DateTimezone('Europe/Berlin'));
+ $event['end']->setTimezone(new DateTimezone('Europe/Berlin'));
+
+ $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'), true);
$this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
- $this->assertContains('METHOD:REQUEST', $ics, "iTip method");
- $this->assertContains('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
+ $this->assertContains('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN");
+ $this->assertContains('TZID:Europe/Berlin', $ics, "Timezone ID");
+ $this->assertContains('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM");
+ $this->assertContains('TZOFFSETTO:+0200', $ics, "Timzone transition TO");
+ $this->assertContains('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
+
+ $this->assertContains('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
$this->assertContains('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
$this->assertContains('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number");
$this->assertContains('CLASS:CONFIDENTIAL', $ics, "Sensitivity => Class");
@@ -471,10 +479,11 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
function test_datetime()
{
- $localtime = libvcalendar::datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
- $localdate = libvcalendar::datetime_prop('DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
- $utctime = libvcalendar::datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
- $asutctime = libvcalendar::datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
+ $ical = new libvcalendar();
+ $localtime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
+ $localdate = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
+ $utctime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
+ $asutctime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
$this->assertContains('TZID=Europe/Berlin', $localtime->serialize());
$this->assertContains('VALUE=DATE', $localdate->serialize());
@@ -482,6 +491,42 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
$this->assertContains('20130901T100000Z', $asutctime->serialize());
}
+ function test_get_vtimezone()
+ {
+ $vtz = libvcalendar::get_vtimezone('Europe/Berlin', strtotime('2014-08-22T15:00:00+02:00'));
+ $this->assertInstanceOf('\Sabre\VObject\Component', $vtz, "VTIMEZONE is a Component object");
+ $this->assertEquals('Europe/Berlin', $vtz->TZID);
+ $this->assertEquals('4', $vtz->{'X-MICROSOFT-CDO-TZID'});
+
+ // check for transition to daylight saving time which is BEFORE the given date
+ $dst = reset($vtz->select('DAYLIGHT'));
+ $this->assertEquals('DAYLIGHT', $dst->name);
+ $this->assertEquals('20140330T010000', $dst->DTSTART);
+ $this->assertEquals('+0100', $dst->TZOFFSETFROM);
+ $this->assertEquals('+0200', $dst->TZOFFSETTO);
+ $this->assertEquals('CEST', $dst->TZNAME);
+
+ // check (last) transition to standard time which is AFTER the given date
+ $std = end($vtz->select('STANDARD'));
+ $this->assertEquals('STANDARD', $std->name);
+ $this->assertEquals('20141026T010000', $std->DTSTART);
+ $this->assertEquals('+0200', $std->TZOFFSETFROM);
+ $this->assertEquals('+0100', $std->TZOFFSETTO);
+ $this->assertEquals('CET', $std->TZNAME);
+
+ // unknown timezone
+ $vtz = libvcalendar::get_vtimezone('America/Foo Bar');
+ $this->assertEquals(false, $vtz);
+
+ // invalid input data
+ $vtz = libvcalendar::get_vtimezone(new DateTime());
+ $this->assertEquals(false, $vtz);
+
+ // DateTimezone as input data
+ $vtz = libvcalendar::get_vtimezone(new DateTimezone('Europe/Istanbul'));
+ $this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
+ }
+
function get_attachment_data($id, $event)
{
return $this->attachment_data;
commit 3b6509405372c23ff057abe4d840c83dcfebba2e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Sep 22 10:34:15 2014 +0200
Fix dialog labels after 'remove' => 'delete' change from folder navigation
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 3d580e8..5f79cac 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -550,7 +550,7 @@ function rcube_calendar_ui(settings)
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
- buttons[rcmail.gettext('remove', 'calendar')] = function() {
+ buttons[rcmail.gettext('delete', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
@@ -2495,7 +2495,7 @@ function rcube_calendar_ui(settings)
if (!event.recurrence) {
buttons.push({
- text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'),
+ text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 7755aa7..630a47b 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -254,9 +254,9 @@ $labels['importerror'] = 'An error occured while importing';
$labels['aclnorights'] = 'You do not have administrator rights on this calendar.';
$labels['changeeventconfirm'] = 'Change event';
-$labels['removeeventconfirm'] = 'Remove event';
+$labels['removeeventconfirm'] = 'Delete event';
$labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?';
-$labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to remove the current event only, this and all future occurences or all occurences of this event?';
+$labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to delete the current event only, this and all future occurences or all occurences of this event?';
$labels['currentevent'] = 'Current';
$labels['futurevents'] = 'Future';
$labels['allevents'] = 'All';
More information about the commits
mailing list