diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index 198c485a0c..c5120fc628 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -1042,9 +1042,10 @@ class calendar_bo * @param string $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in servertime, 'array'=array, or string with date-format * @param array|int $clear_private_infos_users =null if not null, return events with self::ACL_FREEBUSY too, * but call clear_private_infos() with the given users + * @param boolean $read_recurrence =false true: read the exception, not the series master (only for recur_date && $ids=''!) * @return boolean|array event or array of id => event pairs, false if the acl-check went wrong, null if $ids not found */ - function read($ids,$date=null,$ignore_acl=False,$date_format='ts',$clear_private_infos_users=null) + function read($ids,$date=null, $ignore_acl=False, $date_format='ts', $clear_private_infos_users=null, $read_recurrence=false) { if (!$ids) return false; @@ -1056,10 +1057,10 @@ class calendar_bo if ($ignore_acl || is_array($ids) || ($return = $this->check_perms($check,$ids,0,$date_format,$date))) { if (is_array($ids) || !isset(self::$cached_event['id']) || self::$cached_event['id'] != $ids || - self::$cached_event_date_format != $date_format || + self::$cached_event_date_format != $date_format || $read_recurrence || self::$cached_event['recur_type'] != MCAL_RECUR_NONE && self::$cached_event_date != $date) { - $events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0); + $events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0, $read_recurrence); if ($events) { diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index ddc6d8a209..8ec4779195 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -1995,10 +1995,10 @@ class calendar_boupdate extends calendar_bo * * @param array $event * @param array $old_event - * @param Api\DateTime $instance_date For recurring events, this is the date we + * @param Api\DateTime|int|null $instance_date For recurring events, this is the date we * are dealing with */ - function check_move_alarms(Array &$event, Array $old_event = null, Api\DateTime $instance_date = null) + function check_move_alarms(Array &$event, Array $old_event = null, $instance_date = null) { if ($old_event !== null && $event['start'] == $old_event['start']) return; @@ -2008,11 +2008,20 @@ class calendar_boupdate extends calendar_bo $event['alarm'] = $this->so->read_alarms($event['id']); } + if (is_object($instance_date)) + { + if (!is_a($instance_date, 'EGroupware\\Api\\DateTime')) + { + throw new Api\Exception\WrongParameter('$instance_date must be integer or Api\DateTime!'); + } + $instance_date = $instance_date->format('ts'); + } + foreach($event['alarm'] as &$alarm) { - if($event['recur_type'] != MCAL_RECUR_NONE && is_object($instance_date)) + if($event['recur_type'] != MCAL_RECUR_NONE && $instance_date) { - calendar_so::shift_alarm($event, $alarm, $instance_date->format('ts')); + calendar_so::shift_alarm($event, $alarm, $instance_date); } else if ($alarm['time'] !== $time->format('ts') - $alarm['offset']) { diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index eae158fd3e..94e82ce1d0 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -307,9 +307,10 @@ class calendar_so * * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid * @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initital one + * @param boolean $read_recurrence =false true: read the exception, not the series master (only for recur_date && $ids=''!) * @return array|boolean array with cal_id => event array pairs or false if entry not found */ - function read($ids,$recur_date=0) + function read($ids, $recur_date=0, $read_recurrence=false) { //error_log(__METHOD__.'('.array2string($ids).",$recur_date) ".function_backtrace()); $cols = self::get_columns('calendar', $this->cal_table); @@ -322,7 +323,15 @@ class calendar_so { // We want only the parents to match $where['cal_uid'] = $ids; - $where['cal_reference'] = 0; + $where[] = 'cal_deleted IS NULL'; + if ($read_recurrence) + { + $where['cal_recurrence'] = $recur_date; + } + else + { + $where['cal_reference'] = 0; + } } elseif(is_array($ids) && isset($ids[count($ids)-1]) || is_scalar($ids)) // one or more cal_id's { @@ -340,7 +349,7 @@ class calendar_so )); unset($where['cal_id']); } - if ((int) $recur_date) + if ((int) $recur_date && !$read_recurrence) { $where[] = 'cal_start >= '.(int)$recur_date; $group_by = 'GROUP BY '.$cols; @@ -355,6 +364,12 @@ class calendar_so $events =& $this->get_events($this->db->select($this->cal_table, $cols, $where, __LINE__, __FILE__, false, $group_by, 'calendar', 0, $join), $recur_date); + // if we wanted to read the real recurrence, but we have eg. only a virtual one, we need to try again without $read_recurrence + if ((!$events || ($e = current($events)) && $e['deleted']) && $recur_date && $read_recurrence) + { + return $this->read($ids, $recur_date); + } + return $events ? $events : false; } diff --git a/calendar/inc/class.calendar_uiforms.inc.php b/calendar/inc/class.calendar_uiforms.inc.php index ed16d0a94d..dc62be0bf6 100644 --- a/calendar/inc/class.calendar_uiforms.inc.php +++ b/calendar/inc/class.calendar_uiforms.inc.php @@ -1991,7 +1991,8 @@ class calendar_uiforms extends calendar_ui // convert event from servertime returned by calendar_ical to user-time $this->bo->server2usertime($event); - if (($existing_event = $this->bo->read($event['uid'])) && !$existing_event['deleted']) + if (($existing_event = $this->bo->read($event['uid'], $event['recurrence'], false, 'ts', null, true)) && // true = read the exception + !$existing_event['deleted']) { switch(strtolower($ical_method)) { @@ -2113,23 +2114,72 @@ class calendar_uiforms extends calendar_ui break; } } - // set status and send notification / meeting response - if ($this->bo->set_status($event['id'], $user, $status)) + // do we need to update the event itself + elseif (self::event_changed($event, $event['old'])) { - if (!$msg) $msg = lang('Status changed'); + // check if we are allowed to update the event + if($this->bo->check_perms(Acl::EDIT, $event['old'])) + { + if ($event['recurrence'] && !$event['old']['reference'] && ($recur_event = $this->bo->read($event['id']))) + { + // first we need to add the exception to the recurrence master + $recur_event['recur_exception'][] = $event['recurrence']; + // check if we need to move the alarms, because they are next on that exception + $this->bo->check_move_alarms($recur_event, null, $event['recurrence']); + unset($recur_event['start']); unset($recur_event['end']); // no update necessary + unset($recur_event['alarm']); // unsetting alarms too, as they cant be updated without start! + $this->bo->update($recur_event, $ignore_conflicts=true, true, false, true, $msg, true); + + // then we need to create the exception as new event + unset($event['id']); + $event['reference'] = $event['old']['id']; + $event['caldav_name'] = $event['old']['caldav_name']; + } + else + { + // keep all EGroupware only values of existing events plus alarms + unset($event['alarm']); + $event = array_merge($event['old'], $event); + } + unset($event['old']); + + if (($event['id'] = $this->bo->update($event, $ignore_conflicts=true, true, false, true, $msg, true))) + { + $msg[] = lang('Event saved'); + } + else + { + $msg[] = lang('Error saving the event!'); + break; + } + } + else + { + $event['id'] = $event['old']['id']; + $msg[] = lang('Not enough rights to update the event!'); + } + } + else + { + $event['id'] = $event['old']['id']; + } + // set status and send notification / meeting response + if ($this->bo->set_status($event['id'], $user, $status, $event['recurrence'])) + { + $msg[] = lang('Status changed'); } break; case 'apply': // set status and send notification / meeting response - if ($this->bo->set_status($event['id'], $event['ical_sender_uid'], $event['ical_sender_status'])) + if ($this->bo->set_status($event['id'], $event['ical_sender_uid'], $event['ical_sender_status'], $event['recurrence'])) { $msg = lang('Status changed'); } break; case 'cancel': - if ($event['id'] && $this->bo->set_status($event['id'], $user, 'R')) + if ($event['id'] && $this->bo->set_status($event['id'], $user, 'R', $event['recurrence'])) { $msg = lang('Status changed'); } @@ -2155,7 +2205,29 @@ class calendar_uiforms extends calendar_ui break; } $tpl = new Etemplate('calendar.meeting'); - $tpl->exec('calendar.calendar_uiforms.meeting', $event, array(), $readonlys, $event, 2); + $tpl->exec('calendar.calendar_uiforms.meeting', $event, array(), $readonlys, $event+array( + 'old' => $existing_event, + ), 2); + } + + /** + * Check if an event changed and need to be updated + * + * @param array $_a + * @param array $_b + * @return boolean true if there are some changes, false if not + */ + function event_changed($_a, $_b) + { + static $keys_to_check = array('start', 'end', 'title', 'description', 'location', 'participants', + 'recur_type', 'recur_data', 'recur_interval', 'recur_exception'); + + $a = array_intersect_key($_a, array_flip($keys_to_check)); + $b = array_intersect_key($_b, array_flip($keys_to_check)); + + $ret = $a != $b; + error_log(__METHOD__."() returning ".array2string($ret)." diff=".array2string(array_diff_key($a, $b))); + return $ret; } /** diff --git a/calendar/inc/class.calendar_zpush.inc.php b/calendar/inc/class.calendar_zpush.inc.php index f76f875c8f..c0848c9612 100644 --- a/calendar/inc/class.calendar_zpush.inc.php +++ b/calendar/inc/class.calendar_zpush.inc.php @@ -825,7 +825,7 @@ class calendar_zpush implements activesync_plugin_write, activesync_plugin_meeti { $event['recur_enddate'] = Api\DateTime::server2user($message->recurrence->until); } - $event['recur_exceptions'] = array(); + $event['recur_exception'] = array(); if ($message->exceptions) { foreach($message->exceptions as $exception)