diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 4e8995ed..fe7c311a 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -1392,6 +1392,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) { @@ -1425,6 +1433,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']), @@ -1443,16 +1452,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']); } @@ -1607,6 +1627,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 ff6342b5..2c846e74 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(); @@ -1567,6 +1583,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 = $('
  • ') + .attr('data-value', date2servertime(date)) + .html('' + Q($.fullCalendar.formatDate(date, settings['date_format'])) + '') + .appendTo('#edit-recurrence-rdates'); + + $('').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 @@ -2809,13 +2853,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 ca50f27d..44e27294 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -846,6 +846,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 49f8fa7d..e54722a9 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 7b41a31e..7912df86 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -500,6 +500,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']; } @@ -516,8 +525,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; @@ -674,8 +693,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 5c218523..151f5c75 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 009e6c72..6150e653 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 0cb7e66e..4860e4b9 100644 --- a/plugins/calendar/localization/de_CH.inc +++ b/plugins/calendar/localization/de_CH.inc @@ -179,6 +179,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)'; @@ -198,6 +199,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 9e42874c..582df11b 100644 --- a/plugins/calendar/localization/de_DE.inc +++ b/plugins/calendar/localization/de_DE.inc @@ -179,6 +179,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)'; @@ -198,6 +199,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 c99199e2..340ff2e6 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -210,6 +210,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)'; @@ -229,6 +230,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 9138b132..26d93b4f 100644 --- a/plugins/calendar/skins/classic/calendar.css +++ b/plugins/calendar/skins/classic/calendar.css @@ -541,6 +541,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 6e1c2b36..03e47e6a 100644 --- a/plugins/calendar/skins/classic/templates/eventedit.html +++ b/plugins/calendar/skins/classic/templates/eventedit.html @@ -84,6 +84,9 @@
    +
    + +
    diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index d568d385..6b4548b2 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -606,6 +606,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 0ae2b774..4af08f33 100644 --- a/plugins/calendar/skins/larry/templates/eventedit.html +++ b/plugins/calendar/skins/larry/templates/eventedit.html @@ -81,6 +81,9 @@
    +
    + +
    diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index 2e7cbf0e..88f5ed47 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -757,6 +757,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');