Branch 'roundcubemail-plugins-kolab-3.1' - 4 commits - plugins/calendar plugins/libcalendaring plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Mon Mar 17 14:41:01 CET 2014


 plugins/calendar/calendar.php                             |   52 ++++++++++
 plugins/calendar/calendar_ui.js                           |   70 +++++++++++++-
 plugins/calendar/drivers/database/database_driver.php     |    2 
 plugins/calendar/drivers/kolab/kolab_calendar.php         |    8 +
 plugins/calendar/drivers/kolab/kolab_driver.php           |   39 +++++++
 plugins/calendar/lib/Horde_Date_Recurrence.php            |   34 ++++++
 plugins/calendar/lib/calendar_recurrence.php              |   14 ++
 plugins/calendar/lib/calendar_ui.php                      |    8 +
 plugins/calendar/localization/de_CH.inc                   |    3 
 plugins/calendar/localization/de_DE.inc                   |    3 
 plugins/calendar/localization/en_US.inc                   |    2 
 plugins/calendar/skins/classic/calendar.css               |   22 ++++
 plugins/calendar/skins/classic/templates/eventedit.html   |    3 
 plugins/calendar/skins/larry/calendar.css                 |   25 +++++
 plugins/calendar/skins/larry/templates/eventedit.html     |    3 
 plugins/libcalendaring/libcalendaring.php                 |    1 
 plugins/libcalendaring/libvcalendar.php                   |   66 +++++++++++--
 plugins/libcalendaring/tests/libvcalendar.php             |   27 +++++
 plugins/libcalendaring/tests/resources/multiple-rdate.ics |    4 
 plugins/libkolab/lib/kolab_format_xcal.php                |   17 +++
 20 files changed, 379 insertions(+), 24 deletions(-)

New commits:
commit ce84c0642992445e8d897a9411bf5a7186228594
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Mar 17 12:40:21 2014 +0100

    Add full support for arbitrary recurrence dates (RDATE) to the calendar UI

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 0ce1df1..9917c4d 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1240,6 +1240,14 @@ class calendar extends rcube_plugin
       if ($event['recurrence']['UNTIL'])
         $event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c');
       unset($event['recurrence']['EXCEPTIONS']);
+
+      // format RDATE values
+      if (is_array($event['recurrence']['RDATE'])) {
+        $libcal = $this->lib;
+        $event['recurrence']['RDATE'] = array_map(function($rdate) use ($libcal) {
+          return $libcal->adjust_timezone($rdate, true)->format('c');
+        }, $event['recurrence']['RDATE']);
+      }
     }
 
     foreach ((array)$event['attachments'] as $k => $attachment) {
@@ -1273,6 +1281,7 @@ class calendar extends rcube_plugin
       'end'   => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
       // 'changed' might be empty for event recurrences (Bug #2185)
       'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
+      'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null,
       'title'       => strval($event['title']),
       'description' => strval($event['description']),
       'location'    => strval($event['location']),
@@ -1291,16 +1300,27 @@ class calendar extends rcube_plugin
     if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
       $first = $rrule['RDATE'][0];
       $second = $rrule['RDATE'][1];
+      $third  = $rrule['RDATE'][2];
       if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
         $diff = $first->diff($second);
         foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
           if ($diff->$k != 0) {
             $rrule['FREQ'] = $freq;
             $rrule['INTERVAL'] = $diff->$k;
+
+            // verify interval with next item
+            if (is_a($third, 'DateTime')) {
+              $diff2 = $second->diff($third);
+              if ($diff2->$k != $diff->$k) {
+                unset($rrule['INTERVAL']);
+              }
+            }
             break;
           }
         }
       }
+      if (!$rrule['INTERVAL'])
+        $rrule['FREQ'] = 'RDATE';
       $rrule['UNTIL'] = end($rrule['RDATE']);
     }
 
@@ -1455,6 +1475,21 @@ class calendar extends rcube_plugin
     if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL']))
       $event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone);
 
+    if (is_array($event['recurrence']) && is_array($event['recurrence']['RDATE'])) {
+      $tz = $this->timezone;
+      $start = $event['start'];
+      $event['recurrence']['RDATE'] = array_map(function($rdate) use ($tz, $start) {
+        try {
+          $dt = new DateTime($rdate, $tz);
+          $dt->setTime($start->format('G'), $start->format('i'));
+          return $dt;
+        }
+        catch (Exception $e) {
+          return null;
+        }
+      }, $event['recurrence']['RDATE']);
+    }
+
     $attachments = array();
     $eventid = 'cal:'.$event['id'];
     if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index adafb3b..09b4de0 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -514,11 +514,12 @@ function rcube_calendar_ui(settings)
       var recurrence, interval, rrtimes, rrenddate;
       var load_recurrence_tab = function()
       {
-        recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
+        recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ || (event.recurrence.RDATE ? 'RDATE' : '') : '').change();
         interval = $('#eventedit select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
         rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
         rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(parseISO8601(event.recurrence.UNTIL), settings['date_format']) : '');
         $('#eventedit input.edit-recurrence-until:checked').prop('checked', false);
+        $('#edit-recurrence-rdates').html('');
       
         var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
         var rrepeat_id = '#edit-recurrence-repeat-forever';
@@ -551,6 +552,11 @@ function rcube_calendar_ui(settings)
         else if (event.start) {
           $('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]);
         }
+        if (event.recurrence && event.recurrence.RDATE) {
+          $.each(event.recurrence.RDATE, function(i,rdate){
+            add_rdate(parseISO8601(rdate));
+          });
+        }
       };
       
       // show warning if editing a recurring event
@@ -715,6 +721,16 @@ function rcube_calendar_ui(settings)
             if ((byday = $('#edit-recurrence-yearly-byday').val()))
               data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
           }
+          else if (freq == 'RDATE') {
+            data.recurrence = { RDATE:[] };
+            // take selected but not yet added date into account
+            if ($('#edit-recurrence-rdate-input').val() != '') {
+              $('#recurrence-form-rdate input.button.add').click();
+            }
+            $('#edit-recurrence-rdates li').each(function(i, li){
+              data.recurrence.RDATE.push($(li).attr('data-value'));
+            });
+          }
         }
 
         data.calendar = calendars.val();
@@ -1565,6 +1581,34 @@ function rcube_calendar_ui(settings)
         me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
         rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
       }
+    };
+    
+    // add the given date to the RDATE list
+    var add_rdate = function(date)
+    {
+      var li = $('<li>')
+        .attr('data-value', date2servertime(date))
+        .html('<span>' + Q($.fullCalendar.formatDate(date, settings['date_format'])) + '</span>')
+        .appendTo('#edit-recurrence-rdates');
+
+      $('<a>').attr('href', '#del')
+        .addClass('iconbutton delete')
+        .html(rcmail.get_label('delete', 'calendar'))
+        .attr('title', rcmail.get_label('delete', 'calendar'))
+        .appendTo(li);
+    };
+
+    // re-sort the list items by their 'data-value' attribute
+    var sort_rdates = function()
+    {
+      var mylist = $('#edit-recurrence-rdates'),
+        listitems = mylist.children('li').get();
+      listitems.sort(function(a, b) {
+         var compA = $(a).attr('data-value');
+         var compB = $(b).attr('data-value');
+         return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+      })
+      $.each(listitems, function(idx, item) { mylist.append(item); });
     }
     
     // post the given event data to server
@@ -2763,13 +2807,31 @@ function rcube_calendar_ui(settings)
       $('#edit-recurrence-frequency').change(function(e){
         var freq = $(this).val().toLowerCase();
         $('.recurrence-form').hide();
-        if (freq)
-          $('#recurrence-form-'+freq+', #recurrence-form-until').show();
+        if (freq) {
+          $('#recurrence-form-'+freq).show();
+          if (freq != 'rdate')
+            $('#recurrence-form-until').show();
+        }
+      });
+      $('#recurrence-form-rdate input.button.add').click(function(e){
+        var dt, dv = $('#edit-recurrence-rdate-input').val();
+        if (dv && (dt = parse_datetime('12:00', dv))) {
+          add_rdate(dt);
+          sort_rdates();
+          $('#edit-recurrence-rdate-input').val('')
+        }
+        else {
+          $('#edit-recurrence-rdate-input').select();
+        }
+      });
+      $('#edit-recurrence-rdates').on('click', 'a.delete', function(e){
+        $(this).closest('li').remove();
+        return false;
       });
       $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
       $('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); });
 
-      $('#event-export-startdate').datepicker(datepicker_settings);
+      $('#edit-recurrence-rdate-input, #event-export-startdate').datepicker(datepicker_settings);
 
       // init attendees autocompletion
       var ac_props;
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index a2cb903..b4d97ab 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -799,6 +799,8 @@ class database_driver extends calendar_driver
           $rr[2] = intval($rr[2]);
         else if ($rr[1] == 'UNTIL')
           $rr[2] = date_create($rr[2]);
+        else if ($rr[1] == 'RDATE')
+          $rr[2] = array_map('date_create', explode(',', $rr[2]));
         else if ($rr[1] == 'EXDATE')
           $rr[2] = array_map('date_create', explode(',', $rr[2]));
         $event['recurrence'][$rr[1]] = $rr[2];
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 49f8fa7..e54722a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -596,7 +596,7 @@ class kolab_calendar
       unset($record['recurrence']);
 
     // remove internals
-    unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments']);
+    unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
 
     return $record;
   }
@@ -647,9 +647,9 @@ class kolab_calendar
 
     $event['_owner'] = $identity['email'];
 
-    # copy RDATE values as the UI doesn't yet support these
-    if (empty($event['recurrence']['FREQ']) && $old['recurrence']['RDATE'] && empty($old['recurrence']['FREQ'])) {
-      $event['recurrence']['RDATE'] = $old['recurrence']['RDATE'];
+    # remove EXDATE values if RDATE is given
+    if (!empty($event['recurrence']['RDATE'])) {
+      $event['recurrence']['EXDATE'] = array();
     }
 
     // remove some internal properties which should not be saved
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 3eaa7f3..b5635ac 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -466,6 +466,15 @@ class kolab_driver extends calendar_driver
             if ($master['recurrence']['COUNT'])
               $master['recurrence']['COUNT']--;
           }
+          // remove the matching RDATE entry
+          else if ($master['recurrence']['RDATE']) {
+            foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+              if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
+                unset($master['recurrence']['RDATE'][$j]);
+                break;
+              }
+            }
+          }
           else {  // add exception to master event
             $master['recurrence']['EXDATE'][] = $event['start'];
           }
@@ -482,8 +491,18 @@ class kolab_driver extends calendar_driver
             unset($master['recurrence']['COUNT']);
 
             // if all future instances are deleted, remove recurrence rule entirely (bug #1677)
-            if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd'))
+            if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
               $master['recurrence'] = array();
+            }
+            // remove matching RDATE entries
+            else if ($master['recurrence']['RDATE']) {
+              foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+                if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
+                  $master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
+                  break;
+                }
+              }
+            }
 
             $success = $storage->update_event($master);
             break;
@@ -640,8 +659,24 @@ class kolab_driver extends calendar_driver
             }
         }
 
+        $add_exception = true;
+
+        // adjust matching RDATE entry if dates changed
+        if ($savemode == 'current' && $master['recurrence']['RDATE'] && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
+          foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+            if ($rdate->format('Ymd') == $old_date) {
+              $master['recurrence']['RDATE'][$j] = $event['start'];
+              sort($master['recurrence']['RDATE']);
+              $add_exception = false;
+              break;
+            }
+          }
+        }
+
         // save as new exception to master event
-        $master['recurrence']['EXCEPTIONS'][] = $event;
+        if ($add_exception) {
+          $master['recurrence']['EXCEPTIONS'][] = $event;
+        }
         $success = $storage->update_event($master);
         break;
 
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index 5c21852..151f5c7 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -57,12 +57,18 @@ class calendar_recurrence
     $this->engine->fromRRule20(libcalendaring::to_rrule($event['recurrence']));
 
     if (is_array($event['recurrence']['EXDATE'])) {
-      foreach ($event['recurrence']['EXDATE'] as $exdate)
-        $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
+      foreach ($event['recurrence']['EXDATE'] as $exdate) {
+        if (is_a($exdate, 'DateTime')) {
+          $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
+        }
+      }
     }
     if (is_array($event['recurrence']['RDATE'])) {
-      foreach ($event['recurrence']['RDATE'] as $rdate)
-        $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+      foreach ($event['recurrence']['RDATE'] as $rdate) {
+        if (is_a($rdate, 'DateTime')) {
+          $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+        }
+      }
     }
   }
 
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 85e87f8..0fc5af3 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -392,6 +392,7 @@ class calendar_ui
         $select->add($this->cal->gettext('weekly'), 'WEEKLY');
         $select->add($this->cal->gettext('monthly'), 'MONTHLY');
         $select->add($this->cal->gettext('yearly'), 'YEARLY');
+        $select->add($this->cal->gettext('rdate'), 'RDATE');
         $html = html::label('edit-frequency', $this->cal->gettext('frequency')) . $select->show('');
         break;
 
@@ -480,6 +481,13 @@ class calendar_ui
           $this->cal->gettext('untildate') . ' ' . $input->show(''));
         $html = $table->show();
         break;
+
+      case 'rdate':
+        $ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
+        $input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10"));
+        $button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->cal->gettext('addrdate')));
+        $html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
+        break;
     }
 
     return $html;
diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc
index 2360be5..44abe26 100644
--- a/plugins/calendar/localization/de_CH.inc
+++ b/plugins/calendar/localization/de_CH.inc
@@ -202,6 +202,7 @@ $labels['daily'] = 'täglich';
 $labels['weekly'] = 'wöchentlich';
 $labels['monthly'] = 'monatlich';
 $labels['yearly'] = 'jährlich';
+$labels['rdate'] = 'per Datum';
 $labels['every'] = 'Alle';
 $labels['days'] = 'Tag(e)';
 $labels['weeks'] = 'Woche(n)';
@@ -221,7 +222,7 @@ $labels['third'] = 'dritter';
 $labels['fourth'] = 'vierter';
 $labels['last'] = 'letzter';
 $labels['dayofmonth'] = 'Tag des Montats';
-
+$labels['addrdate'] = 'Datum hinzufügen';
 $labels['changeeventconfirm'] = 'Termin ändern';
 $labels['removeeventconfirm'] = 'Termin löschen';
 $labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';
diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc
index f8227e8..d0ef296 100644
--- a/plugins/calendar/localization/de_DE.inc
+++ b/plugins/calendar/localization/de_DE.inc
@@ -202,6 +202,7 @@ $labels['daily'] = 'täglich';
 $labels['weekly'] = 'wöchentlich';
 $labels['monthly'] = 'monatlich';
 $labels['yearly'] = 'jährlich';
+$labels['rdate'] = 'per Datum';
 $labels['every'] = 'Alle';
 $labels['days'] = 'Tag(e)';
 $labels['weeks'] = 'Woche(n)';
@@ -221,7 +222,7 @@ $labels['third'] = 'dritter';
 $labels['fourth'] = 'vierter';
 $labels['last'] = 'letzter';
 $labels['dayofmonth'] = 'Tag des Montats';
-
+$labels['addrdate'] = 'Datum hinzufügen';
 $labels['changeeventconfirm'] = 'Termin ändern';
 $labels['removeeventconfirm'] = 'Termin löschen';
 $labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 5c41ed1..9907472 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -209,6 +209,7 @@ $labels['daily'] = 'daily';
 $labels['weekly'] = 'weekly';
 $labels['monthly'] = 'monthly';
 $labels['yearly'] = 'annually';
+$labels['rdate'] = 'on dates';
 $labels['every'] = 'Every';
 $labels['days'] = 'day(s)';
 $labels['weeks'] = 'week(s)';
@@ -228,6 +229,7 @@ $labels['third'] = 'third';
 $labels['fourth'] = 'fourth';
 $labels['last'] = 'last';
 $labels['dayofmonth'] = 'Day of month';
+$labels['addrdate'] = 'Add repeat date';
 
 $labels['changeeventconfirm'] = 'Change event';
 $labels['removeeventconfirm'] = 'Remove event';
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 77308c2..77175f5 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -537,6 +537,28 @@ td.topalign {
 	margin-left: 7.5em;
 }
 
+#edit-recurrence-rdates {
+	display: block;
+	list-style: none;
+	margin: 0 0 0.8em 0;
+	padding: 0;
+	max-height: 300px;
+	overflow: auto;
+}
+
+#edit-recurrence-rdates li {
+	display: block;
+	position: relative;
+	width: 14em;
+	padding: 1px;
+}
+
+#edit-recurrence-rdates li a.delete {
+	position: absolute;
+	top: 1px;
+	right: 0;
+}
+
 #eventedit .recurrence-form {
 	display: none;
 }
diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html
index 6e1c2b3..03e47e6 100644
--- a/plugins/calendar/skins/classic/templates/eventedit.html
+++ b/plugins/calendar/skins/classic/templates/eventedit.html
@@ -84,6 +84,9 @@
       <div class="recurrence-form" id="recurrence-form-until">
         <roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
       </div>
+      <div class="recurrence-form" id="recurrence-form-rdate">
+        <roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
+      </div>
     </div>
     <!-- attendees list -->
     <div id="event-tab-3">
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index aa5345d..08637f1 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -602,6 +602,31 @@ td.topalign {
 	display: none;
 }
 
+#edit-recurrence-rdates {
+	display: block;
+	list-style: none;
+	margin: 0 0 0.8em 0;
+	padding: 0;
+	max-height: 300px;
+	overflow: auto;
+}
+
+#edit-recurrence-rdates li {
+	display: block;
+	position: relative;
+	width: 12em;
+	padding: 4px 0 4px 0;
+}
+
+#edit-recurrence-rdates li a.delete {
+	position: absolute;
+	top: 2px;
+	right: 0;
+	width: 20px;
+	height: 18px;
+	background-position: -7px -337px;
+}
+
 #eventedit .formtable td {
 	padding: 0.2em 0;
 }
diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html
index 0ae2b77..4af08f3 100644
--- a/plugins/calendar/skins/larry/templates/eventedit.html
+++ b/plugins/calendar/skins/larry/templates/eventedit.html
@@ -81,6 +81,9 @@
 			<div class="recurrence-form" id="recurrence-form-until">
 				<roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
 			</div>
+			<div class="recurrence-form" id="recurrence-form-rdate">
+				<roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
+			</div>
 		</div>
 		<!-- attendees list -->
 		<div id="event-tab-3">
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index d108364..415a15c 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -746,6 +746,7 @@ class libcalendaring extends rcube_plugin
             case 'UNTIL':
                 $val = $val->format('Ymd\THis');
                 break;
+            case 'RDATE':
             case 'EXDATE':
                 foreach ((array)$val as $i => $ex)
                     $val[$i] = $ex->format('Ymd\THis');


commit 5aeceacafeea9a63da54f201929067ddee8581da
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 16:51:55 2014 +0100

    Keep RDATE values when updating an event (the client doesn't submit these values back)

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 2fe072a..49f8fa7 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -346,7 +346,6 @@ class kolab_calendar
     if (!$old || PEAR::isError($old))
       return false;
 
-    $old['recurrence'] = '';  # clear old field, could have been removed in new, too
     $object = $this->_from_rcube_event($event, $old);
     $saved = $this->storage->save($object, 'event', $event['id']);
 
@@ -648,6 +647,11 @@ class kolab_calendar
 
     $event['_owner'] = $identity['email'];
 
+    # copy RDATE values as the UI doesn't yet support these
+    if (empty($event['recurrence']['FREQ']) && $old['recurrence']['RDATE'] && empty($old['recurrence']['FREQ'])) {
+      $event['recurrence']['RDATE'] = $old['recurrence']['RDATE'];
+    }
+
     // remove some internal properties which should not be saved
     unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']);
 


commit 783b4fd64317de077eb2d7c8b41e648ecb9b40d7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 16:16:56 2014 +0100

    Add basic support for RDATE recurrence properties (#2886).
    This only uses these values as a fall-back if no RRULE is defined but not in combination with it.

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 5bf5fdb..0ce1df1 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1287,6 +1287,23 @@ class calendar extends rcube_plugin
    */
   private function _recurrence_text($rrule)
   {
+    // derive missing FREQ and INTERVAL from RDATE list
+    if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
+      $first = $rrule['RDATE'][0];
+      $second = $rrule['RDATE'][1];
+      if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
+        $diff = $first->diff($second);
+        foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
+          if ($diff->$k != 0) {
+            $rrule['FREQ'] = $freq;
+            $rrule['INTERVAL'] = $diff->$k;
+            break;
+          }
+        }
+      }
+      $rrule['UNTIL'] = end($rrule['RDATE']);
+    }
+
     // TODO: finish this
     $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
     $details = '';
diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php
index 81f0857..35f884c 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -127,6 +127,13 @@ class Horde_Date_Recurrence
     public $recurMonths = array();
 
     /**
+     * RDATE recurrence values
+     *
+     * @var array
+     */
+    public $rdates = array();
+
+    /**
      * All the exceptions from recurrence for this event.
      *
      * @var array
@@ -427,7 +434,7 @@ class Horde_Date_Recurrence
             return clone $this->start;
         }
 
-        if ($this->recurInterval == 0) {
+        if ($this->recurInterval == 0 && empty($this->rdates)) {
             return false;
         }
 
@@ -779,6 +786,19 @@ class Horde_Date_Recurrence
             return $next;
         }
 
+        // fall-back to RDATE properties
+        if (!empty($this->rdates)) {
+            $next = clone $this->start;
+            foreach ($this->rdates as $rdate) {
+                $next->year  = $rdate->year;
+                $next->month = $rdate->month;
+                $next->mday  = $rdate->mday;
+                if ($next->compareDateTime($after) > 0) {
+                    return $next;
+                }
+            }
+        }
+
         // We didn't find anything, the recurType was bad, or something else
         // went wrong - return false.
         return false;
@@ -835,6 +855,18 @@ class Horde_Date_Recurrence
     }
 
     /**
+     * Adds an absolute recurrence date.
+     *
+     * @param integer $year   The year of the instance.
+     * @param integer $month  The month of the instance.
+     * @param integer $mday   The day of the month of the instance.
+     */
+    public function addRDate($year, $month, $mday)
+    {
+        $this->rdates[] = new Horde_Date($year, $month, $mday);
+    }
+
+    /**
      * Adds an exception to a recurring event.
      *
      * @param integer $year   The year of the execption.
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index d4a3641..5c21852 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -60,6 +60,10 @@ class calendar_recurrence
       foreach ($event['recurrence']['EXDATE'] as $exdate)
         $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
     }
+    if (is_array($event['recurrence']['RDATE'])) {
+      foreach ($event['recurrence']['RDATE'] as $rdate)
+        $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+    }
   }
 
   /**


commit 64310bdbd78aea41b756862c9cb48fff955292d2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 16:12:24 2014 +0100

    Read/write RDATE properties from/to ical and libkolabxml (#2885)

diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index f468b3c..d07c001 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -367,8 +367,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,
@@ -434,7 +434,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':
@@ -458,7 +462,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,12 +675,20 @@ class libvcalendar implements Iterator
     }
 
     /**
+     *
+     */
+    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();
@@ -692,10 +704,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;
     }
 
@@ -838,8 +878,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) {
@@ -852,6 +897,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 f4db7ec..2bb07ba 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -213,6 +213,19 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
     }
 
     /**
+     * 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
      */
     function test_freebusy()
@@ -363,7 +376,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 51b938d..a501706 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 500dfa2..f751abc 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -191,6 +191,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);
@@ -311,7 +318,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'])
@@ -368,6 +375,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']) {





More information about the commits mailing list