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