diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index bdb6996f..bfab2ad2 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -447,8 +447,8 @@ class calendar extends rcube_plugin if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); - else if ($removed) - $this->rc->output->show_message('calendar.successremoval', 'confirmation'); + else if ($removed) + $this->rc->output->show_message('calendar.successremoval', 'confirmation'); else $this->rc->output->show_message('calendar.errorsaving', 'error'); diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index de4f1dc5..1a57fc5b 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -162,6 +162,16 @@ class kolab_calendar public function get_event($id) { $this->_fetch_events(); + + // event not found, maybe a recurring instance is requested + if (!$this->events[$id]) { + $master_id = preg_replace('/-\d+$/', '', $id); + if ($this->events[$master_id] && $this->events[$master_id]['recurrence']) { + $master = $this->events[$master_id]; + $this->_get_recurring_events($master, $master['start'], $master['start'] + 86400 * 365, $id); + } + } + return $this->events[$id]; } @@ -174,9 +184,6 @@ class kolab_calendar */ public function list_events($start, $end, $search = null) { - // use Horde classes to compute recurring instances - require_once 'Horde/Date/Recurrence.php'; - $this->_fetch_events(); $events = array(); @@ -205,33 +212,9 @@ class kolab_calendar $events[] = $event; } - // resolve recurring events (maybe move to _fetch_events() for general use?) + // resolve recurring events if ($event['recurrence']) { - $recurrence = new Horde_Date_Recurrence($event['start']); - $recurrence->fromRRule20(calendar::to_rrule($event['recurrence'])); - - foreach ((array)$event['recurrence']['EXDATE'] as $exdate) - $recurrence->addException(date('Y', $exdate), date('n', $exdate), date('j', $exdate)); - - $duration = $event['end'] - $event['start']; - $next = new Horde_Date($event['start']); - $i = 1; - while ($next = $recurrence->nextActiveRecurrence(array('year' => $next->year, 'month' => $next->month, 'mday' => $next->mday + 1, 'hour' => $next->hour, 'min' => $next->min, 'sec' => $next->sec))) { - $rec_start = $next->timestamp(); - $rec_end = $rec_start + $duration; - - // add to output if in range - if ($rec_start <= $end && $rec_end >= $start) { - $rec_event = $event; - $rec_event['id'] .= '-' . $i++; - $rec_event['recurrence_id'] = $event['id']; - $rec_event['start'] = $rec_start; - $rec_event['end'] = $rec_end; - $events[] = $rec_event; - } - else if ($rec_start > $end) // stop loop if out of range - break; - } + $events = array_merge($events, $this->_get_recurring_events($event, $start, $end)); } } @@ -334,6 +317,52 @@ class kolab_calendar } } + + /** + * Create instances of a recurring event + */ + private function _get_recurring_events($event, $start, $end, $event_id = null) + { + // use Horde classes to compute recurring instances + require_once 'Horde/Date/Recurrence.php'; + + $recurrence = new Horde_Date_Recurrence($event['start']); + $recurrence->fromRRule20(calendar::to_rrule($event['recurrence'])); + + foreach ((array)$event['recurrence']['EXDATE'] as $exdate) + $recurrence->addException(date('Y', $exdate), date('n', $exdate), date('j', $exdate)); + + $events = array(); + $duration = $event['end'] - $event['start']; + $next = new Horde_Date($event['start']); + $i = 0; + while ($next = $recurrence->nextActiveRecurrence(array('year' => $next->year, 'month' => $next->month, 'mday' => $next->mday + 1, 'hour' => $next->hour, 'min' => $next->min, 'sec' => $next->sec))) { + $rec_start = $next->timestamp(); + $rec_end = $rec_start + $duration; + $rec_id = $event['id'] . '-' . ++$i; + + // add to output if in range + if (($rec_start <= $end && $rec_end >= $start) || ($event_id && $rec_id == $event_id)) { + $rec_event = $event; + $rec_event['id'] = $rec_id; + $rec_event['recurrence_id'] = $event['id']; + $rec_event['start'] = $rec_start; + $rec_event['end'] = $rec_end; + $rec_event['_instance'] = $i; + $events[] = $rec_event; + + if ($rec_id == $event_id) { + $this->events[$rec_id] = $rec_event; + break; + } + } + else if ($rec_start > $end) // stop loop if out of range + break; + } + + return $events; + } + /** * Convert from Kolab_Format to internal representation */ @@ -394,7 +423,7 @@ class kolab_calendar if ($recurrence['exclusion']) { foreach ((array)$recurrence['exclusion'] as $excl) - $rrule['EXDATE'][] = strtotime($excl) - $this->cal->timezone * 3600 - date('Z'); // shift to user's timezone + $rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date'])); // use time of event start } } @@ -428,9 +457,12 @@ class kolab_calendar { $priority_map = $this->priority_map; $daymap = array('MO'=>'monday','TU'=>'tuesday','WE'=>'wednesday','TH'=>'thursday','FR'=>'friday','SA'=>'saturday','SU'=>'sunday'); + $monthmap = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'); + $tz_offset = $this->cal->timezone * 3600; $object = array( // kolab => roundcube + 'uid' => $event['uid'], 'summary' => $event['title'], 'location' => $event['location'], 'body' => $event['description'], @@ -478,20 +510,22 @@ class kolab_calendar } //weekly - if ($ra['FREQ']=='WEEKLY') { - $weekdays = split(",", $ra['BYDAY']); - foreach ($weekdays as $days) { - $weekly[] = $daymap[$days]; + if ($ra['FREQ'] == 'WEEKLY') { + if ($ra['BYDAY']) { + foreach (split(",", $ra['BYDAY']) as $day) + $object['recurrence']['day'][] = $daymap[$day]; + } + else { + // use weekday of start date if empty + $object['recurrence']['day'][] = strtolower(gmdate('l', $event['start'] + $tz_offset)); } - - $object['recurrence']['day'] = $weekly; } //monthly (temporary hack to follow current Horde logic) - if ($ra['FREQ']=='MONTHLY') { - if ($ra['BYDAY'] == 'NaN') { - $object['recurrence']['daynumber']=1; - $object['recurrence']['day'] = array(date('L', $event['start'])); + if ($ra['FREQ'] == 'MONTHLY') { + if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) { + $object['recurrence']['daynumber'] = $m[1]; + $object['recurrence']['day'] = array($daymap[$m[2]]); $object['recurrence']['cycle'] = 'monthly'; $object['recurrence']['type'] = 'weekday'; } @@ -502,17 +536,36 @@ class kolab_calendar } } - //year ??? + //yearly + if ($ra['FREQ'] == 'YEARLY') { + if (!$ra['BYMONTH']) + $ra['BYMONTH'] = gmdate('n', $event['start'] + $tz_offset); + + $object['recurrence']['cycle'] = 'yearly'; + $object['recurrence']['month'] = $monthmap[intval($ra['BYMONTH'])]; + + if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) { + $object['recurrence']['type'] = 'weekday'; + $object['recurrence']['daynumber'] = $m[1]; + $object['recurrence']['day'] = array($daymap[$m[2]]); + } + else { + $object['recurrence']['type'] = 'monthday'; + $object['recurrence']['daynumber'] = gmdate('j', $event['start'] + $tz_offset); + } + } //exclusions - $object['recurrence']['exclusion'] = (array)$ra['EXDATE']; + foreach ((array)$ra['EXDATE'] as $excl) { + $object['recurrence']['exclusion'][] = gmdate('Y-m-d', $excl + $tz_offset); + } } // whole day event if ($event['allday']) { $object['end-date'] += 60; // end is at 23:59 => jump to the next day - $object['end-date'] += $this->cal->timezone * 3600 - date('Z'); // shift 00 times from user's timezone to server's timezone - $object['start-date'] += $this->cal->timezone * 3600 - date('Z'); // because Horde_Kolab_Format_Date::encodeDate() uses strftime() + $object['end-date'] += $tz_offset - date('Z'); // shift 00 times from user's timezone to server's timezone + $object['start-date'] += $tz_offset - date('Z'); // because Horde_Kolab_Format_Date::encodeDate() uses strftime() $object['_is_all_day'] = 1; } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 4f6ff144..5f448404 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -207,14 +207,14 @@ class kolab_driver extends calendar_driver { if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) { $folder = $cal->get_realname(); - if (rcube_kolab::folder_delete($folder)) { + if (rcube_kolab::folder_delete($folder)) { // remove color in user prefs (temp. solution) $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); unset($prefs['kolab_calendars'][$prop['id']]); $this->rc->user->save_prefs($prefs); return true; - } + } } return false; @@ -243,10 +243,7 @@ class kolab_driver extends calendar_driver */ public function edit_event($event) { - if ($storage = $this->calendars[$event['calendar']]) - return $storage->update_event($event); - - return false; + return $this->update_event($event); } /** @@ -258,7 +255,7 @@ class kolab_driver extends calendar_driver public function move_event($event) { if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) - return $storage->update_event($event + $ev); + return $this->update_event($event + $ev); return false; } @@ -272,7 +269,7 @@ class kolab_driver extends calendar_driver public function resize_event($event) { if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) - return $storage->update_event($event + $ev); + return $this->update_event($event + $ev); return false; } @@ -286,10 +283,141 @@ class kolab_driver extends calendar_driver */ public function remove_event($event) { - if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) - return $storage->delete_event($event); + $success = false; + + if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) { + $savemode = 'all'; + $master = $event; + + // read master if deleting a recurring event + if ($event['recurrence'] || $event['recurrence_id']) { + $master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event; + $savemode = $event['savemode']; + } + + switch ($savemode) { + case 'current': + // add exception to master event + $master['recurrence']['EXDATE'][] = $event['start']; + $success = $storage->update_event($master); + break; + + case 'future': + if ($master['id'] != $event['id']) { + // set until-date on master event + $master['recurrence']['UNTIL'] = $event['start'] - 86400; + unset($master['recurrence']['COUNT']); + $success = $storage->update_event($master); + break; + } + + default: // 'all' is default + $success = $storage->delete_event($master); + break; + } + } - return false; + return $success; + } + + /** + * Wrapper to update an event object depending on the given savemode + */ + private function update_event($event) + { + if (!($storage = $this->calendars[$event['calendar']])) + return false; + + $success = false; + $savemode = 'all'; + $old = $master = $storage->get_event($event['id']); + + // modify a recurring event, check submitted savemode to do the right things + if ($old['recurrence'] || $old['recurrence_id']) { + $master = $old['recurrence_id'] ? $storage->get_event($old['recurrence_id']) : $old; + $savemode = $event['savemode']; + } + + // keep saved exceptions (not submitted by the client) + if ($old['recurrence']['EXDATE']) + $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE']; + + switch ($savemode) { + case 'new': + // save submitted data as new (non-recurring) event + $event['recurrence'] = array(); + $event['uid'] = $this->cal->generate_uid(); + $success = $storage->insert_event($event); + break; + + case 'current': + // add exception to master event + $master['recurrence']['EXDATE'][] = $old['start']; + $storage->update_event($master); + + // insert new event for this occurence + $event += $old; + $event['recurrence'] = array(); + $event['uid'] = $this->cal->generate_uid(); + $success = $storage->insert_event($event); + break; + + case 'future': + if ($master['id'] != $event['id']) { + // set until-date on master event + $master['recurrence']['UNTIL'] = $old['start'] - 86400; + unset($master['recurrence']['COUNT']); + $storage->update_event($master); + + // save this instance as new recurring event + $event += $old; + $event['uid'] = $this->cal->generate_uid(); + + // if recurrence COUNT, update value to the correct number of future occurences + if ($event['recurrence']['COUNT']) { + $event['recurrence']['COUNT'] -= $old['_instance']; + } + + // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::insert_event() + if (strlen($event['recurrence']['BYDAY']) == 2) + unset($event['recurrence']['BYDAY']); + if ($event['recurrence']['BYMONTH']) + unset($event['recurrence']['BYMONTH']); + + $success = $storage->insert_event($event); + break; + } + + default: // 'all' is default + $event['id'] = $master['id']; + $event['uid'] = $master['uid']; + + // use start date from master but try to be smart on time or duration changes + $old_start_date = date('Y-m-d', $old['start']); + $old_start_time = date('H:i:s', $old['start']); + $old_duration = $old['end'] - $old['start']; + + $new_start_date = date('Y-m-d', $event['start']); + $new_start_time = date('H:i:s', $event['start']); + $new_duration = $event['end'] - $event['start']; + + // shifted or resized + if ($old_start_date == $new_start_date || $old_duration == $new_duration) { + $event['start'] = $master['start'] + ($event['start'] - $old['start']); + $event['end'] = $event['start'] + $new_duration; + + // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event() + if (strlen($event['recurrence']['BYDAY']) == 2) + unset($event['recurrence']['BYDAY']); + if ($event['recurrence']['BYMONTH']) + unset($event['recurrence']['BYMONTH']); + } + + $success = $storage->update_event($event); + break; + } + + return $success; } /** diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index cca4470b..e897e4cb 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -321,7 +321,7 @@ class calendar_ui $select->add($this->calendar->gettext('daily'), 'DAILY'); $select->add($this->calendar->gettext('weekly'), 'WEEKLY'); $select->add($this->calendar->gettext('monthly'), 'MONTHLY'); - // $select->add($this->calendar->gettext('yearly'), 'YEARLY'); + $select->add($this->calendar->gettext('yearly'), 'YEARLY'); $html = html::label('edit-frequency', $this->calendar->gettext('frequency')) . $select->show(''); break; @@ -350,39 +350,24 @@ class calendar_ui case 'monthly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly')); $html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('months'))); - // day of month selection - - //hidden fields to emulate user selection: - $input_bymonthday = new html_hiddenfield(array('name' => 'bymonthday', 'value' => 1)); - $input_byday = new html_hiddenfield(array('name' => 'byday', 'value' => 1)); - $input_bydayp = new html_hiddenfield(array('name' => 'bydayprefix', 'value' => 1)); - - $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode')); - $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); - $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->calendar->gettext('on_the_same_weekday'))); - $table->add('label', html::label(null, $radio->show('', array('value' => 'BYMONTHDAY')) . ' ' . $this->calendar->gettext('on_the_same_date'))); - $html .= $input_byday->show(); - $html .= $input_bymonthday->show(); - $html .= $input_bydayp->show(); - $html .= html::div($attrib, $table->show()); - - /* + +/* multiple month selection is not supported by Kolab $checkbox = new html_radiobutton(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday')); for ($monthdays = '', $d = 1; $d <= 31; $d++) { $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d); $monthdays .= $d % 7 ? ' ' : html::br(); } - +*/ // rule selectors $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode')); $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); - $table->add('label topalign', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->calendar->gettext('each'))); + $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->calendar->gettext('onsamedate'))); // $this->calendar->gettext('each') $table->add(null, $monthdays); $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->calendar->gettext('onevery'))); $table->add(null, $this->rrule_selectors($attrib['part'])); $html .= html::div($attrib, $table->show()); - */ + break; // annually recurrence form @@ -391,9 +376,10 @@ class calendar_ui $html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('years'))); // month selector $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'); - $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth')); + $boxtype = is_a($this->calendar->driver, 'kolab_driver') ? 'radio' : 'checkbox'; + $checkbox = new html_inputfield(array('type' => $boxtype, 'name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth')); for ($months = '', $m = 1; $m <= 12; $m++) { - $months .= html::label(array('class' => 'month'), $checkbox->show('', array('value' => $m)) . $this->calendar->gettext($monthmap[$m])); + $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->calendar->gettext($monthmap[$m])); $months .= $m % 4 ? ' ' : html::br(); } $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months); @@ -453,10 +439,13 @@ class calendar_ui $this->calendar->gettext('first'), $this->calendar->gettext('second'), $this->calendar->gettext('third'), - $this->calendar->gettext('fourth'), - $this->calendar->gettext('last') + $this->calendar->gettext('fourth') ), - array(1, 2, 3, 4, -1)); + array(1, 2, 3, 4)); + + // Kolab doesn't support 'last' but others do. + if (!is_a($this->calendar->driver, 'kolab_driver')) + $select_prefix->add($this->calendar->gettext('last'), -1); $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday")); if ($noselect) $select_wday->add($noselect, ''); diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 72e3c5b4..7a8585e4 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -58,8 +58,6 @@ $labels['prev_month'] = 'Previous month'; $labels['next_year'] = 'Next year'; $labels['next_month'] = 'Next month'; $labels['choose_date'] = 'Choose date'; -$labels['on_the_same_date'] = 'on the same date'; -$labels['on_the_same_weekday'] = 'on the same weekday'; // alarm/reminder settings @@ -121,6 +119,7 @@ $labels['bydays'] = 'On'; $labels['until'] = 'the'; $labels['each'] = 'Each'; $labels['onevery'] = 'On every'; +$labels['onsamedate'] = 'On the same date'; $labels['forever'] = 'forever'; $labels['recurrencend'] = 'until'; $labels['forntimes'] = 'for $nr time(s)';