Include VTIMEZONE definitions when exporting event data or invitations as iCal (#3199)
This commit is contained in:
parent
3b65094053
commit
f9b19b9f27
2 changed files with 189 additions and 23 deletions
|
@ -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 ***/
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue