diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -857,8 +857,8 @@ if ($success && $reload) $this->rc->output->command('plugin.reload_view'); } - - + + /** * Dispatcher for event actions initiated by the client */ @@ -867,7 +867,7 @@ $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true); $success = $reload = $got_msg = false; - + // force notify if hidden + active if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1) $event['_notify'] = 1; @@ -887,8 +887,10 @@ case "new": // create UID for new event $event['uid'] = $this->generate_uid(); - $this->write_preprocess($event, $action); - if ($success = $this->driver->new_event($event)) { + if (!$this->write_preprocess($event, $action)) { + $got_msg = true; + } + else if ($success = $this->driver->new_event($event)) { $event['id'] = $event['uid']; $event['_savemode'] = 'all'; $this->cleanup_event($event); @@ -898,8 +900,10 @@ break; case "edit": - $this->write_preprocess($event, $action); - if ($success = $this->driver->edit_event($event)) { + if (!$this->write_preprocess($event, $action)) { + $got_msg = true; + } + else if ($success = $this->driver->edit_event($event)) { $this->cleanup_event($event); $this->event_save_success($event, $old, $action, $success); } @@ -907,19 +911,23 @@ break; case "resize": - $this->write_preprocess($event, $action); - if ($success = $this->driver->resize_event($event)) { + if (!$this->write_preprocess($event, $action)) { + $got_msg = true; + } + else if ($success = $this->driver->resize_event($event)) { $this->event_save_success($event, $old, $action, $success); } $reload = $event['_savemode'] ? 2 : 1; break; case "move": - $this->write_preprocess($event, $action); - if ($success = $this->driver->move_event($event)) { + if (!$this->write_preprocess($event, $action)) { + $got_msg = true; + } + else if ($success = $this->driver->move_event($event)) { $this->event_save_success($event, $old, $action, $success); } - $reload = $success && $event['_savemode'] ? 2 : 1; + $reload = $success && $event['_savemode'] ? 2 : 1; break; case "remove": @@ -1184,7 +1192,7 @@ // unlock client $this->rc->output->command('plugin.unlock_saving'); - // update event object on the client or trigger a complete refretch if too complicated + // update event object on the client or trigger a complete refresh if too complicated if ($reload) { $args = array('source' => $event['calendar']); if ($reload > 1) @@ -1993,12 +2001,31 @@ // start/end is all we need for 'move' action (#1480) if ($action == 'move') { - return; + return true; } // convert the submitted recurrence settings if (is_array($event['recurrence'])) { $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']); + + // align start date with the first occurrence + if (!empty($event['recurrence']) && !empty($event['syncstart']) + && (empty($event['_savemode']) || $event['_savemode'] == 'all') + ) { + $next = $this->find_first_occurrence($event); + + if (!$next) { + $this->rc->output->show_message('calendar.recurrenceerror', 'error'); + return false; + } + else if ($event['start'] != $next) { + $diff = $event['start']->diff($event['end'], true); + + $event['start'] = $next; + $event['end'] = clone $next; + $event['end']->add($diff); + } + } } // convert the submitted alarm values @@ -2075,6 +2102,8 @@ $event['url'] = $event['vurl']; unset($event['vurl']); } + + return true; } /** @@ -3448,6 +3477,35 @@ } /** + * Find first occurrence of a recurring event excluding start date + * + * @param array $event Event data (with 'start' and 'recurrence') + * + * @return DateTime Date of the first occurrence + */ + public function find_first_occurrence($event) + { + // Make sure libkolab plugin is loaded in case of Kolab driver + $this->load_driver(); + + // Use libkolab to compute recurring events (and libkolab plugin) + // Horde-based fallback has many bugs + if (class_exists('kolabformat') && class_exists('kolabcalendaring') && class_exists('kolab_date_recurrence')) { + $object = kolab_format::factory('event', 3.0); + $object->set($event); + + $recurrence = new kolab_date_recurrence($object); + } + else { + // fallback to libcalendaring (Horde-based) recurrence implementation + require_once(__DIR__ . '/lib/calendar_recurrence.php'); + $recurrence = new calendar_recurrence($this, $event); + } + + return $recurrence->first_occurrence(); + } + + /** * Magic getter for public access to protected members */ public function __get($name) diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -678,7 +678,7 @@ var freebusy = $('#edit-free-busy').val(event.free_busy); var priority = $('#edit-priority').val(event.priority); var sensitivity = $('#edit-sensitivity').val(event.sensitivity); - + var syncstart = $('#edit-recurrence-syncstart input'); var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000); var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration); var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show(); @@ -898,6 +898,9 @@ data._fromcalendar = event.calendar; } + if (data.recurrence && syncstart.is(':checked')) + data.syncstart = 1; + update_event(action, data); $dialog.dialog("close"); } // end click: @@ -3974,8 +3977,15 @@ $('#edit-attendees-form .attendees-invitebox').show(); } } + // reset autocompletion on tab change (#3389) rcmail.ksearch_blur(); + + // display recurrence warning in recurrence tab only + if (tab == 'recurrence') + $('#edit-recurrence-frequency').change(); + else + $('#edit-recurrence-syncstart').hide(); } }); $('#edit-enddate').datepicker(datepicker_settings); diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -659,14 +659,7 @@ } // use libkolab to compute recurring events - if (class_exists('kolabcalendaring')) { - $recurrence = new kolab_date_recurrence($object); - } - else { - // fallback to local recurrence implementation - require_once($this->cal->home . '/lib/calendar_recurrence.php'); - $recurrence = new calendar_recurrence($this->cal, $event); - } + $recurrence = new kolab_date_recurrence($object); $i = 0; while ($next_event = $recurrence->next_instance()) { @@ -717,7 +710,7 @@ if (++$i > 100000) break; } - + return $events; } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -1784,16 +1784,16 @@ */ private function get_recurrence_count($event, $dtstart) { - // use libkolab to compute recurring events - if (class_exists('kolabcalendaring') && $event['_formatobj']) { - $recurrence = new kolab_date_recurrence($event['_formatobj']); - } - else { - // fallback to local recurrence implementation - require_once($this->cal->home . '/lib/calendar_recurrence.php'); - $recurrence = new calendar_recurrence($this->cal, $event); + // load the given event data into a libkolabxml container + if (!$event['_formatobj']) { + $event_xml = new kolab_format_event(); + $event_xml->set($event); + $event['_formatobj'] = $event_xml; } + // use libkolab to compute recurring events + $recurrence = new kolab_date_recurrence($event['_formatobj']); + $count = 0; while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) { $count++; diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -92,6 +92,7 @@ $this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar')); $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table')); $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify')); + $this->cal->register_handler('plugin.edit_recurrence_sync', array($this, 'edit_recurrence_sync')); $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning')); $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons')); $this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options')); @@ -473,7 +474,7 @@ } /** - * + * Render HTML for attendee notification warning */ function edit_attendees_notify($attrib = array()) { @@ -482,6 +483,15 @@ } /** + * Render HTML for recurrence option to align start date with the recurrence rule + */ + function edit_recurrence_sync($attrib = array()) + { + $checkbox = new html_checkbox(array('name' => '_start_sync', 'value' => 1)); + return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('eventstartsync'))); + } + + /** * Generate the form for recurrence settings */ function recurring_event_warning($attrib = array()) diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -125,6 +125,7 @@ $labels['invitationsdeclined'] = 'Declined invitations'; $labels['changepartstat'] = 'Change participant status'; $labels['rsvpcomment'] = 'Invitation text'; +$labels['eventstartsync'] = 'Move the event start date to the first occurrence'; // agenda view $labels['listrange'] = 'Range to display:'; @@ -267,6 +268,7 @@ $labels['futurevents'] = 'Future'; $labels['allevents'] = 'All'; $labels['saveasnew'] = 'Save as new'; +$labels['recurrenceerror'] = 'Unable to resolve recurrence rule for specified start date.'; // birthdays calendar $labels['birthdays'] = 'Birthdays'; diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html --- a/plugins/calendar/skins/larry/templates/eventedit.html +++ b/plugins/calendar/skins/larry/templates/eventedit.html @@ -127,7 +127,8 @@ +