diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 128263613c..f333de2e1c 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -6,7 +6,7 @@ * @package calendar * @author Ralf Becker * @author Joerg Lehrke - * @copyright (c) 2005-12 by RalfBecker-At-outdoor-training.de + * @copyright (c) 2005-15 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ @@ -92,11 +92,12 @@ class calendar_boupdate extends calendar_bo * updates or creates an event, it (optionaly) checks for conflicts and sends the necessary notifications * * @param array &$event event-array, on return some values might be changed due to set defaults - * @param boolean $ignore_conflicts=false just ignore conflicts or do a conflict check and return the conflicting events - * @param boolean $touch_modified=true NOT USED ANYMORE (was only used in old csv-import), modified&modifier is always updated! - * @param boolean $ignore_acl=false should we ignore the acl - * @param boolean $updateTS=true update the content history of the event + * @param boolean $ignore_conflicts =false just ignore conflicts or do a conflict check and return the conflicting events + * @param boolean $touch_modified =true NOT USED ANYMORE (was only used in old csv-import), modified&modifier is always updated! + * @param boolean $ignore_acl =false should we ignore the acl + * @param boolean $updateTS =true update the content history of the event * @param array &$messages=null messages about because of missing ACL removed participants or categories + * @param boolean $skip_notification =false true: send NO notifications, default false = send them * @return mixed on success: int $cal_id > 0, on error false or array with conflicting events (only if $check_conflicts) * Please note: the events are not garantied to be readable by the user (no read grant or private)! * @@ -141,8 +142,7 @@ class calendar_boupdate extends calendar_bo // set owner as participant if none is given if (!is_array($event['participants']) || !count($event['participants'])) { - $status = $event['owner'] == $this->user ? 'A' : 'U'; - $status = calendar_so::combine_status($status, 1, 'CHAIR'); + $status = calendar_so::combine_status($event['owner'] == $this->user ? 'A' : 'U', 1, 'CHAIR'); $event['participants'] = array($event['owner'] => $status); } } @@ -205,6 +205,7 @@ class calendar_boupdate extends calendar_bo { foreach((array)$event['participants'] as $uid => $status) { + $q = $r = null; calendar_so::split_status($status,$q,$r); if ($status != 'U') { @@ -399,7 +400,7 @@ class calendar_boupdate extends calendar_bo * Remove participants current user has no right to invite * * @param array &$event new event - * @param array $old_event=null old event with already invited participants + * @param array $old_event =null old event with already invited participants * @return array removed participants because of missing invite grants */ public function remove_no_acl_invite(array &$event,array $old_event=null) @@ -413,7 +414,7 @@ class calendar_boupdate extends calendar_bo $old_event = $this->read($event['id']); } $removed = array(); - foreach((array)$event['participants'] as $uid => $status) + foreach(array_keys((array)$event['participants']) as $uid) { if ((is_null($old_event) || !isset($old_event['participants'][$uid])) && !$this->check_acl_invite($uid)) { @@ -479,7 +480,7 @@ class calendar_boupdate extends calendar_bo } } // Find new participants ... - foreach((array)$new_event['participants'] as $new_userid => $new_status) + foreach(array_keys((array)$new_event['participants']) as $new_userid) { if(!isset($old_event['participants'][$new_userid])) { @@ -590,8 +591,8 @@ class calendar_boupdate extends calendar_bo * Check calendar prefs, if a given user (integer account_id) or email (user or externals) should get notified * * @param int|string $user_or_email - * @param string $ical_method='REQUEST' - * @param string $role='REQ-PARTICIPANT' + * @param string $ical_method ='REQUEST' + * @param string $role ='REQ-PARTICIPANT' * @return boolean true if user requested to be notified, false if not */ static public function email_update_requested($user_or_email, $ical_method='REQUEST', $role='REQ-PARTICIPANT') @@ -710,8 +711,8 @@ class calendar_boupdate extends calendar_bo * @param int $msg_type type of the notification: MSG_ADDED, MSG_MODIFIED, MSG_ACCEPTED, ... * @param array $to_notify numerical user-ids as keys (!) (value is not used) * @param array $old_event Event before the change - * @param array $new_event=null Event after the change - * @param int $user=0 User who started the notify, default current user + * @param array $new_event =null Event after the change + * @param int $user =0 User who started the notify, default current user * @return bool true/false */ function send_update($msg_type,$to_notify,$old_event,$new_event=null,$user=0) @@ -1004,6 +1005,7 @@ class calendar_boupdate extends calendar_bo function get_update_message($event,$added) { + $nul = null; $details = $this->_get_event_details($event,$added ? lang('Added') : lang('Modified'),$nul); $notify_msg = $this->cal_prefs[$added || empty($this->cal_prefs['notifyModified']) ? 'notifyAdded' : 'notifyModified']; @@ -1051,7 +1053,8 @@ class calendar_boupdate extends calendar_bo $alarm['time'] = $this->date2ts($event['start']) - $alarm['offset']; unset($alarm['times']); unset($alarm['next']); - $this->save_alarm($alarm['cal_id'],$alarm); + //error_log(__METHOD__."() moving alarm to next recurrence ".array2string($alarm)); + $this->save_alarm($alarm['cal_id'], $alarm, false); // false = do NOT update timestamp, as nothing changed for iCal clients } return $ret; } @@ -1062,8 +1065,8 @@ class calendar_boupdate extends calendar_bo * This methode converts from user to server time and handles the insertion of users and dates of repeating events * * @param array $event - * @param boolean $ignore_acl=false should we ignore the acl - * @param boolean $updateTS=true update the content history of the event + * @param boolean $ignore_acl =false should we ignore the acl + * @param boolean $updateTS =true update the content history of the event * @return int|boolean $cal_id > 0 or false on error (eg. permission denied) */ function save($event,$ignore_acl=false,$updateTS=true) @@ -1095,8 +1098,7 @@ class calendar_boupdate extends calendar_bo { if (!empty($event['start'])) { - $time = new egw_time($event['start'], egw_time::$user_timezone); - $time = $this->so->startOfDay($time); + $time = $this->so->startOfDay(new egw_time($event['start'], egw_time::$user_timezone)); $event['start'] = egw_time::to($time, 'ts'); $save_event['start'] = $time; } @@ -1109,14 +1111,12 @@ class calendar_boupdate extends calendar_bo } if (!empty($event['recurrence'])) { - $time = new egw_time($event['recurrence'], egw_time::$user_timezone); - $time = $this->so->startOfDay($time); + $time = $this->so->startOfDay(new egw_time($event['recurrence'], egw_time::$user_timezone)); $event['recurrence'] = egw_time::to($time, 'ts'); } if (!empty($event['recur_enddate'])) { - $time = new egw_time($event['recur_enddate'], egw_time::$user_timezone); - $time = $this->so->startOfDay($time); + $time = $this->so->startOfDay(new egw_time($event['recur_enddate'], egw_time::$user_timezone)); $event['recur_enddate'] = egw_time::to($time, 'ts'); $time->setUser(); $save_event['recur_enddate'] = egw_time::to($time, 'ts'); @@ -1147,8 +1147,7 @@ class calendar_boupdate extends calendar_bo { if ($event['whole_day']) { - $time = new egw_time($date, egw_time::$user_timezone); - $time =& $this->so->startOfDay($time); + $time = $this->so->startOfDay(new egw_time($date, egw_time::$user_timezone)); $date = egw_time::to($time, 'ts'); } else @@ -1161,11 +1160,8 @@ class calendar_boupdate extends calendar_bo // same with the alarms if (isset($event['alarm']) && is_array($event['alarm']) && isset($event['start'])) { - foreach($event['alarm'] as $id => $alarm) + foreach($event['alarm'] as $id => &$alarm) { - // recalculate alarms to also cope with moved events (beside server time adjustment) - $event['alarm'][$id]['time'] = $event['start'] - $alarm['offset']; - // remove alarms belonging to not longer existing or rejected participants if ($alarm['owner'] && isset($event['participants'])) { @@ -1182,18 +1178,19 @@ class calendar_boupdate extends calendar_bo // update all existing alarm times, in case alarm got moved and alarms are not include in $event if ($old_event && is_array($old_event['alarm']) && isset($event['start'])) { - foreach($old_event['alarm'] as $id => $alarm) + foreach($old_event['alarm'] as $id => &$alarm) { if (!isset($event['alarm'][$id])) { $alarm['time'] = $event['start'] - $alarm['offset']; - // remove (not store) alarms belonging to not longer existing or rejected participants + if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm); + // remove (not store) alarms belonging to not longer existing or rejected participants $status = isset($event['participants']) ? $event['participants'][$alarm['owner']] : $old_event['participants'][$alarm['owner']]; if (!$alarm['owner'] || isset($status) && calendar_so::split_status($status) !== 'R') { - $this->so->save_alarm($event['id'], $alarm, $this->now); - error_log(__LINE__.': '.__METHOD__."() so->save_alarm($event[id], ".array2string($alarm).", $this->now)"); + $this->so->save_alarm($event['id'], $alarm); + error_log(__LINE__.': '.__METHOD__."() so->save_alarm($event[id], ".array2string($alarm).")"); } else { @@ -1229,6 +1226,7 @@ class calendar_boupdate extends calendar_bo { foreach($event['participants'] as $uid => $status) { + $user_type = $user_id = null; calendar_so::split_user($uid, $user_type, $user_id); if ($user_type == 'c' && (!$old_event || !isset($old_event['participants'][$uid]))) { @@ -1332,7 +1330,7 @@ class calendar_boupdate extends calendar_bo /** * Get rights for a given category id * - * @param int $cat_id=null null to return array with all cats + * @param int $cat_id =null null to return array with all cats * @return array with account_id => right pairs */ public static function get_cat_rights($cat_id=null) @@ -1384,7 +1382,7 @@ class calendar_boupdate extends calendar_bo */ public static function has_cat_right($right,$cat_id,$user) { - static $cache; + static $cache=null; if (!isset($cache[$cat_id])) { @@ -1392,7 +1390,7 @@ class calendar_boupdate extends calendar_bo $cat_rights = self::get_cat_rights($cat_id); if (!is_null($cat_rights)) { - static $memberships; + static $memberships=null; if (is_null($memberships)) { $memberships = $GLOBALS['egw']->accounts->memberships($user,true); @@ -1432,6 +1430,7 @@ class calendar_boupdate extends calendar_bo { return false; } + $quantity = $role = null; calendar_so::split_status($status, $quantity, $role); if ($this->log) { @@ -1486,7 +1485,7 @@ class calendar_boupdate extends calendar_bo * * @param array $new_event event-array with the new stati * @param array $old_event event-array with the old stati - * @param int $recur_date=0 date to change, or 0 = all since now + * @param int $recur_date =0 date to change, or 0 = all since now * @param boolean $skip_notification Do not send notifications. Parameter passed on to set_status(). */ function update_status($new_event, $old_event , $recur_date=0, $skip_notification=false) @@ -1517,10 +1516,10 @@ class calendar_boupdate extends calendar_bo * deletes an event * * @param int $cal_id id of the event to delete - * @param int $recur_date=0 if a single event from a series should be deleted, its date - * @param boolean $ignore_acl=false true for no ACL check, default do ACL check - * @param boolean $skip_notification=false - * @param boolean $delete_exceptions=true true: delete, false: keep exceptions (with new UID) + * @param int $recur_date =0 if a single event from a series should be deleted, its date + * @param boolean $ignore_acl =false true for no ACL check, default do ACL check + * @param boolean $skip_notification =false + * @param boolean $delete_exceptions =true true: delete, false: keep exceptions (with new UID) * @param int &$exceptions_kept=null on return number of kept exceptions * @return boolean true on success, false on error (usually permission denied) */ @@ -1594,6 +1593,7 @@ class calendar_boupdate extends calendar_bo // check if deleted recurrance has alarms (because it's the next recurrance) --> move it to next recurrance if ($event['alarm']) { + $next_recurrance = null; foreach($event['alarm'] as &$alarm) { if (($alarm['time'] == $recur_date) || ($alarm['time']+$alarm['offset'] == $recur_date)) @@ -1604,19 +1604,10 @@ class calendar_boupdate extends calendar_bo { $checkdate = $recur_date; //if ($alarm['time']+$alarm['offset'] == $recur_date) $checkdate = $recur_date + $alarm['offset']; - if ($e = $this->read($cal_id,$checkdate+1)) + if (($e = $this->read($cal_id,$checkdate+1))) { $next_recurrance = $this->date2ts($e['start']); } - /* - foreach(calendar_rrule::event2rrule($event, true) as $time) - { - $time->setUser(); // $time is in timezone of event, convert it to usertime used here - $next_recurrance = $this->date2ts($time); - if ($next_recurrance > $checkdate) break; - } - */ - //error_log(__METHOD__.__LINE__." $next_recurrance > $checkdate"); } $alarm['time'] = $this->date2ts($next_recurrance, true); // user to server-time $alarm['cal_id'] = $cal_id; @@ -1644,6 +1635,11 @@ class calendar_boupdate extends calendar_bo /** * helper for send_update and get_update_message * @internal + * @param array $event + * @param string $action + * @param array $event_arr + * @param array $disinvited + * @return array */ function _get_event_details($event,$action,&$event_arr,$disinvited=array()) { @@ -1787,8 +1783,8 @@ class calendar_boupdate extends calendar_bo * * @param array $event2save event-data before calling save * @param array $event_saved event-data read back from the DB - * @param array $old_event=null event-data in the DB before calling save - * @param string $type='update' + * @param array $old_event =null event-data in the DB before calling save + * @param string $type ='update' */ function log2file($event2save,$event_saved,$old_event=null,$type='update') { @@ -1820,9 +1816,10 @@ class calendar_boupdate extends calendar_bo * * @param int $cal_id Id of the calendar-entry * @param array $alarm array with fields: text, owner, enabled, .. + * @param boolean $update_modified =true call update modified, default true * @return string id of the alarm, or false on error (eg. no perms) */ - function save_alarm($cal_id,$alarm) + function save_alarm($cal_id, $alarm, $update_modified=true) { if (!$cal_id || !$this->check_perms(EGW_ACL_EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0)) { @@ -1833,7 +1830,7 @@ class calendar_boupdate extends calendar_bo $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar', $cal_id, 'modify', $this->now); - return $this->so->save_alarm($cal_id,$alarm); + return $this->so->save_alarm($cal_id, $alarm, $update_modified); } /** @@ -1861,7 +1858,7 @@ class calendar_boupdate extends calendar_bo * currently used for ical/sif import * * @param array $catname_list names of the categories which should be found or added - * @param int|array $old_event=null match against existing event and expand the returned category ids + * @param int|array $old_event =null match against existing event and expand the returned category ids * by the ones the user normally does not see due to category permissions - used to preserve categories * @return array category ids (found, added and preserved categories) */ @@ -2191,7 +2188,7 @@ class calendar_boupdate extends calendar_bo if ($filter == 'master') { // We found the master - $matchingEvents = array($egwEvent['id']);; + $matchingEvents = array($egwEvent['id']); break; } if ($filter == 'exact') @@ -2446,14 +2443,14 @@ class calendar_boupdate extends calendar_bo } // append pseudos as last entries - $matchingEvents = array_merge($matchingEvents, $pseudos); + $matches = array_merge($matchingEvents, $pseudos); if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. - '[MATCHES]:' . array2string($matchingEvents)."\n",3,$this->logfile); + '[MATCHES]:' . array2string($matches)."\n",3,$this->logfile); } - return $matchingEvents; + return $matches; } /** @@ -2493,8 +2490,8 @@ class calendar_boupdate extends calendar_bo { $type = 'SERIES-PSEUDO-EXCEPTION'; $wasPseudo = true; - list($eventID, $recur_date) = explode(':', $eventID); - $recur_date = $this->date2usertime($recur_date); + list($eventID, $date) = explode(':', $eventID); + $recur_date = $this->date2usertime($date); $stored_event = $this->read($eventID, $recur_date, false, 'server'); $master_event = $this->read($eventID, 0, false, 'server'); $recurrence_event = $stored_event; diff --git a/calendar/inc/class.calendar_groupdav.inc.php b/calendar/inc/class.calendar_groupdav.inc.php index cc27acfb67..7c7959c831 100644 --- a/calendar/inc/class.calendar_groupdav.inc.php +++ b/calendar/inc/class.calendar_groupdav.inc.php @@ -7,7 +7,7 @@ * @package calendar * @subpackage groupdav * @author Ralf Becker - * @copyright (c) 2007-13 by Ralf Becker + * @copyright (c) 2007-15 by Ralf Becker * @version $Id$ */ @@ -135,7 +135,7 @@ class calendar_groupdav extends groupdav_handler * @param array &$options * @param array &$files * @param int $user account_id - * @param string $id='' + * @param string $id ='' * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') */ function propfind($path,&$options,&$files,$user,$id='') @@ -157,6 +157,7 @@ class calendar_groupdav extends groupdav_handler 'daywise' => false, 'date_format' => 'server', 'no_total' => true, // we need no total number of rows (saves extra query) + 'cfs' => array(), // return custom-fields, as we use them to store X- attributes ); foreach(array( 'start' => $GLOBALS['egw_info']['user']['preferences']['groupdav']['calendar-past-limit'], @@ -252,7 +253,7 @@ class calendar_groupdav extends groupdav_handler * * @param string $path * @param array $filter - * @param array|boolean $start=false false=return all or array(start,num) + * @param array|boolean $start =false false=return all or array(start,num) * @return array with "files" array with values for keys path and props */ function propfind_callback($path,array $filter,$start=false) @@ -289,6 +290,7 @@ class calendar_groupdav extends groupdav_handler $files[] = array('path' => $path.urldecode($this->get_path($event))); continue; } + $schedule_tag = null; $etag = $this->get_etag($event, $schedule_tag); //header('X-EGROUPWARE-EVENT-'.$event['id'].': '.$event['title'].': '.date('Y-m-d H:i:s',$event['start']).' - '.date('Y-m-d H:i:s',$event['end'])); @@ -555,7 +557,7 @@ class calendar_groupdav extends groupdav_handler * * @param array &$options * @param int $id - * @param int $user=null account_id + * @param int $user =null account_id * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') */ function get(&$options,$id,$user=null) @@ -568,6 +570,7 @@ class calendar_groupdav extends groupdav_handler $options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null); $options['mimetype'] = 'text/calendar; charset=utf-8'; header('Content-Encoding: identity'); + $schedule_tag = null; header('ETag: "'.$this->get_etag($event, $schedule_tag).'"'); if ($this->use_schedule_tag) { @@ -582,9 +585,9 @@ class calendar_groupdav extends groupdav_handler * Taking into account virtual an real exceptions for recuring events * * @param array $event - * @param int $user=null account_id of calendar to display - * @param string $method=null eg. 'PUBLISH' for inbox, nothing anywhere else - * @param boolean|array $expand=false true or array with values for 'start', 'end' to expand recurrences + * @param int $user =null account_id of calendar to display + * @param string $method =null eg. 'PUBLISH' for inbox, nothing anywhere else + * @param boolean|array $expand =false true or array with values for 'start', 'end' to expand recurrences * @return string */ private function iCal(array $event,$user=null, $method=null, $expand=false) @@ -626,9 +629,9 @@ class calendar_groupdav extends groupdav_handler * Maybe that should be part of calendar_bo * * @param string $uid UID - * @param calendar_bo $bo=null calendar_bo object to reuse for search call - * @param boolean|array $expand=false true or array with values for 'start', 'end' to expand recurrences - * @param int $user=null account_id of calendar to display, to remove master, if current user does not participate in + * @param calendar_bo $bo =null calendar_bo object to reuse for search call + * @param boolean|array $expand =false true or array with values for 'start', 'end' to expand recurrences + * @param int $user =null account_id of calendar to display, to remove master, if current user does not participate in * @return array */ private static function &get_series($uid,calendar_bo $bo=null, $expand=false, $user=null) @@ -640,6 +643,7 @@ class calendar_groupdav extends groupdav_handler 'filter' => 'owner', // return all possible entries 'daywise' => false, 'date_format' => 'server', + 'cfs' => array(), // read cfs as we use them to store X- attributes ); if (is_array($expand)) $params += $expand; @@ -690,6 +694,13 @@ class calendar_groupdav extends groupdav_handler } continue; // nothing to change } + // alarms are reported on recurrences --> move them to master + foreach($recurrence['alarm'] as $alarm) + { + $master['alarm'][] = $alarm; + } + $recurrence['alarm'] = array(); + // now we need to check if this recurrence is an exception if (!$expand && $master['participants'] == $recurrence['participants']) { @@ -731,8 +742,8 @@ class calendar_groupdav extends groupdav_handler * * @param array &$options * @param int $id - * @param int $user=null account_id of owner, default null - * @param string $prefix=null user prefix from path (eg. /ralf from /ralf/addressbook) + * @param int $user =null account_id of owner, default null + * @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook) * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') */ function put(&$options,$id,$user=null,$prefix=null) @@ -811,6 +822,7 @@ class calendar_groupdav extends groupdav_handler { $schedule_tag_match = $_SERVER['HTTP_IF_SCHEDULE_TAG_MATCH']; if ($schedule_tag_match[0] == '"') $schedule_tag_match = substr($schedule_tag_match, 1, -1); + $schedule_tag = null; $this->get_etag($oldEvent, $schedule_tag); if ($schedule_tag_match !== $schedule_tag) @@ -835,6 +847,7 @@ class calendar_groupdav extends groupdav_handler if (($events = $handler->icaltoegw($vCalendar))) { $modified = 0; + $series = null; foreach($events as $n => $event) { // for recurrances of event series, we need to read correct recurrence (or if series master is no first event) @@ -883,7 +896,8 @@ class calendar_groupdav extends groupdav_handler // import alarms, if given and changed if ((array)$event['alarm'] !== (array)$oldEvent['alarm']) { - $modified += $this->sync_alarms($oldEvent['id'], (array)$event['alarm'], (array)$oldEvent['alarm'], $user, $event['start']); + $event['id'] = $oldEvent['id']; + $modified += $handler->sync_alarms($event, (array)$oldEvent['alarm'], $user); } } if (!$modified) // NO modififictions, or none we understood --> log it and return Ok: "204 No Content" @@ -951,59 +965,12 @@ class calendar_groupdav extends groupdav_handler return $retval; } - /** - * Sync alarms of current user: add alarms added on client and remove the ones removed - * - * @param int $cal_id of event to set alarms - * @param array $alarms - * @param array $old_alarms - * @param int $user account_id of user to create alarm for - * @param int $start start-time of event - * @ToDo store other alarm properties like: ACTION, DESCRIPTION, X-WR-ALARMUID - * @return int number of modified alarms - */ - private function sync_alarms($cal_id, array $alarms, array $old_alarms, $user, $start) - { - if ($this->debug) error_log(__METHOD__."($cal_id, ".array2string($alarms).', '.array2string($old_alarms).", $user, $start)"); - $modified = 0; - foreach($alarms as $alarm) - { - if ($alarm['owner'] != $this->user) continue; // only import alarms of current user - - // check if alarm is already stored or from other users - foreach($old_alarms as $id => $old_alarm) - { - if ($old_alarm['owner'] != $user || $alarm['offset'] == $old_alarm['offset']) - { - unset($old_alarms[$id]); // remove alarms of other user, or already existing alarms - } - } - // alarm not found --> add it - if ($alarm['offset'] != $old_alarm['offset'] || $old_alarm['owner'] != $user) - { - $alarm['owner'] = $user; - $alarm['time'] = $start - $alarm['offset']; - if ($this->debug) error_log(__METHOD__."() adding new alarm from client ".array2string($alarm)); - $this->bo->save_alarm($cal_id, $alarm); - ++$modified; - } - } - // remove all old alarms left from current user - foreach($old_alarms as $id => $old_alarm) - { - if ($this->debug) error_log(__METHOD__."() deleting alarm '$id' deleted on client ".array2string($old_alarm)); - $this->bo->delete_alarm($id); - ++$modified; - } - return $modified; - } - /** * Handle post request for a schedule entry * * @param array &$options * @param int $id - * @param int $user=null account_id of owner, default null + * @param int $user =null account_id of owner, default null * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') */ function post(&$options,$id,$user=null) @@ -1057,8 +1024,8 @@ class calendar_groupdav extends groupdav_handler $handler = $this->_get_handler(); if (($foundEvents = $handler->search($vCalendar, null, false, $charset))) { - $eventId = array_shift($foundEvents); - list($eventId) = explode(':', $eventId); + $id = array_shift($foundEvents); + list($eventId) = explode(':', $id); if (!($cal_id = $handler->importVCal($vCalendar, $eventId, null, false, 0, $this->groupdav->current_user_principal, $user, $charset))) @@ -1082,6 +1049,8 @@ class calendar_groupdav extends groupdav_handler */ protected function outbox_freebusy_request($ical, $charset, $user, array &$options) { + unset($options); // not used, but required by function signature + $vcal = new Horde_Icalendar(); if (!$vcal->parsevCalendar($ical, 'VCALENDAR', $charset)) { @@ -1108,7 +1077,7 @@ class calendar_groupdav extends groupdav_handler $organizer = $component->getAttribute('ORGANIZER'); $attendees = (array)$component->getAttribute('ATTENDEE'); // X-CALENDARSERVER-MASK-UID specifies to exclude given event from busy-time - $mask_uid = $component->getAttribute('X-CALENDARSERVER-MASK-UID'); + $mask_uid = $component->getAttributeDefault('X-CALENDARSERVER-MASK-UID', null); header('Content-type: text/xml; charset=UTF-8'); @@ -1118,7 +1087,7 @@ class calendar_groupdav extends groupdav_handler $xml->startDocument('1.0', 'UTF-8'); $xml->startElementNs('C', 'schedule-response', groupdav::CALDAV); - foreach($event['participants'] as $uid => $status) + foreach(array_keys($event['participants']) as $uid) { $xml->startElementNs('C', 'response', null); @@ -1155,6 +1124,7 @@ class calendar_groupdav extends groupdav_handler */ function free_busy_report($path,$options,$user) { + unset($path); // unused, but required by function signature if (!$this->bo->check_perms(EGW_ACL_FREEBUSY, 0, $user)) { return '403 Forbidden'; @@ -1180,7 +1150,7 @@ class calendar_groupdav extends groupdav_handler * Reimplemented to add read-free-busy and schedule-deliver privilege * * @param string $path path of collection - * @param int $user=null owner of the collection, default current user + * @param int $user =null owner of the collection, default current user * @return array with privileges */ public function current_user_privileges($path, $user=null) @@ -1306,7 +1276,7 @@ class calendar_groupdav extends groupdav_handler // check if user is a participant or one of the groups he is a member of --> reject the meeting request $ret = '403 Forbidden'; $memberships = $GLOBALS['egw']->accounts->memberships($this->bo->user, true); - foreach($event['participants'] as $uid => $status) + foreach(array_keys($event['participants']) as $uid) { if ($this->bo->user == $uid || in_array($uid, $memberships)) { @@ -1391,7 +1361,8 @@ class calendar_groupdav extends groupdav_handler /** * Get the etag for an entry * - * @param array|int $event array with event or cal_id + * @param array|int $entry array with event or cal_id + * @param string $schedule_tag =null on return schedule-tag * @return string|boolean string with etag or false */ function get_etag($entry, &$schedule_tag=null) @@ -1410,10 +1381,11 @@ class calendar_groupdav extends groupdav_handler * @param int|array $entry id or array of new created entry * @param string $path * @param int|string $retval - * @param boolean $path_attr_is_name=true true: path_attr is ca(l|rd)dav_name, false: id (GroupDAV needs Location header) + * @param boolean $path_attr_is_name =true true: path_attr is ca(l|rd)dav_name, false: id (GroupDAV needs Location header) */ function put_response_headers($entry, $path, $retval, $path_attr_is_name=true) { + $schedule_tag = null; $etag = $this->get_etag($entry, $schedule_tag); if ($this->use_schedule_tag) @@ -1443,15 +1415,16 @@ class calendar_groupdav extends groupdav_handler /** * Add extra properties for calendar collections * - * @param array $props=array() regular props by the groupdav handler + * @param array $props regular props by the groupdav handler * @param string $displayname - * @param string $base_uri=null base url of handler - * @param int $user=null account_id of owner of current collection - * @param string $path=null path of the collection + * @param string $base_uri =null base url of handler + * @param int $user =null account_id of owner of current collection + * @param string $path =null path of the collection * @return array */ - public function extra_properties(array $props=array(), $displayname, $base_uri=null, $user=null, $path=null) + public function extra_properties(array $props, $displayname, $base_uri=null, $user=null, $path=null) { + unset($base_uri); // unused, but required by function signature if (!isset($props['calendar-description'])) { // default calendar description: can be overwritten via PROPPATCH, in which case it's already set @@ -1523,8 +1496,8 @@ class calendar_groupdav extends groupdav_handler function get_shared() { $shared = array(); - $calendar_home_set = $GLOBALS['egw_info']['user']['preferences']['groupdav']['calendar-home-set']; - $calendar_home_set = $calendar_home_set ? explode(',',$calendar_home_set) : array(); + $pref = $GLOBALS['egw_info']['user']['preferences']['groupdav']['calendar-home-set']; + $calendar_home_set = $pref ? explode(',', $pref) : array(); // replace symbolic id's with real nummeric id's foreach(array( 'G' => $GLOBALS['egw_info']['user']['account_primary_group'], diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index b9d436999d..2fffd50379 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -251,9 +251,9 @@ class calendar_ical extends calendar_boupdate { if ($this->read($event, $recurrence, true, 'server')) { - if ($this->bo->check_perms(EGW_ACL_FREEBUSY, $event, 0, 'server')) + if ($this->check_perms(EGW_ACL_FREEBUSY, $event, 0, 'server')) { - $this->bo->clear_private_infos($event, array($this->user, $event['owner'])); + $this->clear_private_infos($event, array($this->user, $event['owner'])); } else { @@ -631,7 +631,10 @@ class calendar_ical extends calendar_boupdate else // $version == '2.0' { $attributes['RRULE'] = ''; - $parameters['RRULE'] = $rrule; + foreach($rrule as $n => $v) + { + $attributes['RRULE'] .= ($attributes['RRULE']?';':'').$n.'='.$v; + } } break; @@ -833,6 +836,25 @@ class calendar_ical extends calendar_boupdate } } + // for CalDAV add all X-Properties previously parsed + if ($this->productManufacturer == 'groupdav' || $this->productManufacturer == 'file') + { + foreach($event as $name => $value) + { + if (substr($name, 0, 2) == '##') + { + if (($attr = json_decode($value, true)) && is_array($attr)) + { + $vevent->setAttribute(substr($name, 2), $attr['value'], $attr['params'], true, $attr['values']); + } + else + { + $vevent->setAttribute(substr($name, 2), $value); + } + } + } + } + if ($this->productManufacturer == 'nokia') { if ($event['special'] == '1') @@ -862,7 +884,7 @@ class calendar_ical extends calendar_boupdate foreach ((array)$event['alarm'] as $alarmData) { // skip over alarms that don't have the minimum required info - if (!$alarmData['offset'] && !$alarmData['time']) continue; + if (!isset($alarmData['offset']) && !isset($alarmData['time'])) continue; // skip alarms not being set for all users and alarms owned by other users if ($alarmData['all'] != true && $alarmData['owner'] != $this->user) @@ -899,8 +921,9 @@ class calendar_ical extends calendar_boupdate // VCalendar 2.0 / RFC 2445 // RFC requires DESCRIPTION for DISPLAY - if (!$event['title'] && !$description) continue; + if (!$event['title'] && !$description) $description = 'Alarm'; + /* Disabling for now // Lightning infinitly pops up alarms for recuring events, if the only use an offset if ($this->productName == 'lightning' && $event['recur_type'] != MCAL_RECUR_NONE) { @@ -914,7 +937,7 @@ class calendar_ical extends calendar_boupdate { continue; } - } + }*/ // for SyncML non-whole-day events always use absolute times // (probably because some devices have no clue about timezones) @@ -930,7 +953,7 @@ class calendar_ical extends calendar_boupdate if ($alarmData['offset'] !== false) { $valarm->setAttribute('TRIGGER', -$alarmData['offset'], - array('VALUE' => 'DURATION', 'RELATED' => 'START')); + array('VALUE' => 'DURATION', 'RELATED' => 'START')); } else { @@ -938,9 +961,28 @@ class calendar_ical extends calendar_boupdate $value = self::getDateTime($alarmData['time'],$tzid,$params); $valarm->setAttribute('TRIGGER', $value, $params); } - - $valarm->setAttribute('ACTION','DISPLAY'); - $valarm->setAttribute('DESCRIPTION',$event['title'] ? $event['title'] : $description); + if (!empty($alarmData['uid'])) + { + $valarm->setAttribute('UID', $alarmData['uid']); + $valarm->setAttribute('X-WR-ALARMUID', $alarmData['uid']); + } + // set evtl. existing attributes set by iCal clients not used by EGroupware + if (isset($alarmData['attrs'])) + { + foreach($alarmData['attrs'] as $attr => $data) + { + $valarm->setAttribute($attr, $data['value'], $data['params']); + } + } + // set default ACTION and DESCRIPTION, if not set by a client + if (!isset($alarmData['attrs']) || !isset($alarmData['attrs']['ACTION'])) + { + $valarm->setAttribute('ACTION','DISPLAY'); + } + if (!isset($alarmData['attrs']) || !isset($alarmData['attrs']['DESCRIPTION'])) + { + $valarm->setAttribute('DESCRIPTION',$event['title'] ? $event['title'] : $description); + } $vevent->addComponent($valarm); } } @@ -968,6 +1010,7 @@ class calendar_ical extends calendar_boupdate { $paramData['ENCODING'] = 'QUOTED-PRINTABLE'; } + /* disable automatic QP encoding eg. for new-lines as it also encodes non-ascii/utf-8 which TB does NOT decode else { $paramData['CHARSET'] = ''; @@ -979,7 +1022,7 @@ class calendar_ical extends calendar_boupdate { $paramData['ENCODING'] = ''; } - } + }*/ break; case 'funambol': $paramData['ENCODING'] = 'FUNAMBOL-QP'; @@ -1216,6 +1259,14 @@ class calendar_ical extends calendar_boupdate } } } + // unset old X-* attributes stored in custom-fields + foreach ($event_info['stored_event'] as $key => $value) + { + if ($key[0] == '#' && $key[1] == '#' && !isset($event[$key])) + { + $event[$key] = ''; + } + } if ($merge) { if ($this->log) @@ -1422,46 +1473,9 @@ class calendar_ical extends calendar_boupdate case 'SERIES-MASTER': case 'SERIES-EXCEPTION': case 'SERIES-EXCEPTION-PROPAGATE': - if (count($event['alarm']) > 0) + if (isset($event['alarm'])) { - foreach ($event['alarm'] as $newid => &$alarm) - { - if (!isset($alarm['offset']) && isset($alarm['time'])) - { - $alarm['offset'] = $event['start'] - $alarm['time']; - } - elseif (!isset($alarm['time']) && isset($alarm['offset'])) - { - $alarm['time'] = $event['start'] - $alarm['offset']; - } - $alarm['owner'] = $this->user; - $alarm['all'] = false; - - // if no edit rights, allow participants to set alarms directly (like status) - if ($event_info['stored_event'] && !$event_info['acl_edit']) - { - if ($alarm['time'] < time() && !calendar_so::shift_alarm($event, $alarm)) - { - continue; //pgoerzen: don't add an alarm in the past - } - $this->save_alarm($event_info['stored_event']['id'], $alarm); - } - - if (is_array($event_info['stored_event']) - && count($event_info['stored_event']['alarm']) > 0) - { - foreach ($event_info['stored_event']['alarm'] as $alarm_id => $alarm_data) - { - if ($alarm['offset'] == $alarm_data['offset'] && - ($alarm_data['all'] || $alarm_data['owner'] == $this->user)) - { - unset($event['alarm'][$newid]); - unset($event_info['stored_event']['alarm'][$alarm_id]); - continue 2; - } - } - } - } + $this->sync_alarms($event, (array)$event_info['stored_event']['alarm'], $this->user); } break; @@ -1469,18 +1483,6 @@ class calendar_ical extends calendar_boupdate // nothing to do here break; } - if (is_array($event_info['stored_event']) - && count($event_info['stored_event']['alarm']) > 0) - { - foreach ($event_info['stored_event']['alarm'] as $alarm_id => $alarm_data) - { - // only touch own alarms - if ($alarm_data['all'] == false && $alarm_data['owner'] == $this->user) - { - $this->delete_alarm($alarm_id); - } - } - } } if ($this->log) @@ -1764,6 +1766,73 @@ class calendar_ical extends calendar_boupdate return $updated_id === 0 ? 0 : $return_id; } + /** + * Sync alarms of current user: add alarms added on client and remove the ones removed + * + * @param array& $event + * @param array $old_alarms + * @param int $user account_id of user to create alarm for + * @return int number of modified alarms + */ + public function sync_alarms(array &$event, array $old_alarms, $user) + { + if ($this->debug) error_log(__METHOD__."(".array2string($event).', old_alarms='.array2string($old_alarms).", $user,)"); + $modified = 0; + foreach($event['alarm'] as &$alarm) + { + // check if alarm is already stored or from other users + foreach($old_alarms as $id => $old_alarm) + { + // not current users alarm --> ignore + if (!$old_alarm['all'] && $old_alarm['owner'] != $user) + { + unset($old_alarm[$id]); + continue; + } + // alarm found --> stop + if (empty($alarm['uid']) && $alarm['offset'] == $old_alarm['offset'] || $alarm['uid'] && $alarm['uid'] == $old_alarm['uid']) + { + unset($old_alarms[$id]); + break; + } + } + // alarm not found --> add it + if ($alarm['offset'] != $old_alarm['offset'] || $old_alarm['owner'] != $user && !$alarm['all']) + { + $alarm['owner'] = $user; + if (!isset($alarm['time'])) $alarm['time'] = $event['start'] - $alarm['offset']; + if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm); + if ($this->debug) error_log(__METHOD__."() adding new alarm from client ".array2string($alarm)); + if ($event['id']) $alarm['id'] = $this->save_alarm($event['id'], $alarm); + ++$modified; + } + // existing alarm --> update it + elseif ($alarm['offset'] == $old_alarm['offset'] && ($old_alarm['owner'] == $user || $old_alarm['all'])) + { + if (!isset($alarm['time'])) $alarm['time'] = $event['start'] - $alarm['offset']; + if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm); + $alarm = array_merge($old_alarm, $alarm); + if ($this->debug) error_log(__METHOD__."() updating existing alarm from client ".array2string($alarm)); + $alarm['id'] = $this->save_alarm($event['id'], $alarm); + ++$modified; + } + } + // remove all old alarms left from current user + foreach($old_alarms as $id => $old_alarm) + { + // not current users alarm --> ignore + if (!$old_alarm['all'] && $old_alarm['owner'] != $user) + { + unset($old_alarm[$id]); + continue; + } + if ($this->debug) error_log(__METHOD__."() deleting alarm '$id' deleted on client ".array2string($old_alarm)); + $this->delete_alarm($id); + ++$modified; + } + return $modified; + } + /** * get the value of an attribute by its name * @@ -1784,9 +1853,17 @@ class calendar_ical extends calendar_boupdate return false; } - static function valarm2egw(&$alarms, &$valarm) + /** + * Parsing a valarm component preserving all attributes unknown to EGw + * + * @param array &$alarms on return alarms parsed + * @param Horde_Icalendar_Valarm $valarm valarm component + * @param int $duration in seconds to be able to convert RELATED=END + * @return int number of parsed alarms + */ + static function valarm2egw(&$alarms, Horde_Icalendar_Valarm $valarm, $duration) { - $count = 0; + $alarm = array(); foreach ($valarm->getAllAttributes() as $vattr) { switch ($vattr['name']) @@ -1797,40 +1874,47 @@ class calendar_ical extends calendar_boupdate switch ($vtype) { case 'DURATION': - if (isset($vattr['params']['RELATED']) - && $vattr['params']['RELATED'] != 'START') + if (isset($vattr['params']['RELATED']) && $vattr['params']['RELATED'] == 'END') + { + $alarm['offset'] = $duration -$vattr['value']; + } + elseif (isset($vattr['params']['RELATED']) && $vattr['params']['RELATED'] != 'START') { error_log("Unsupported VALARM offset anchor ".$vattr['params']['RELATED']); + return; } else { - $alarms[] = array('offset' => -$vattr['value']); - $count++; + $alarm['offset'] = -$vattr['value']; } break; case 'DATE-TIME': - $alarms[] = array('time' => $vattr['value']); - $count++; + $alarm['time'] = $vattr['value']; break; default: - // we should also do ;RELATED=START|END error_log('VALARM/TRIGGER: unsupported value type:' . $vtype); } break; - case 'ACTION': - case 'DISPLAY': - case 'DESCRIPTION': - case 'SUMMARY': - case 'ATTACH': - case 'ATTENDEE': - // we ignore these fields silently - break; - default: - error_log('VALARM field ' .$vattr['name'] . ': ' . $vattr['value'] . ' HAS NO CONVERSION YET'); + case 'UID': + case 'X-WR-ALARMUID': + $alarm['uid'] = $vattr['value']; + break; + + default: // store all other attributes, so we dont loose them + $alarm['attrs'][$vattr['name']] = array( + 'params' => $vattr['params'], + 'value' => $vattr['value'], + ); } } - return $count; + if (isset($alarm['offset']) || isset($alarm['time'])) + { + //error_log(__METHOD__."(..., ".$valarm->exportvCalendar().", $duration) alarm=".array2string($alarm)); + $alarms[] = $alarm; + return 1; + } + return 0; } function setSupportedFields($_productManufacturer='', $_productName='') @@ -2246,7 +2330,7 @@ class calendar_ical extends calendar_boupdate { if (is_a($valarm, 'Horde_Icalendar_Valarm')) { - $this->valarm2egw($alarms, $valarm); + self::valarm2egw($alarms, $valarm, $event['end'] - $event['start']); } } $event['alarm'] = $alarms; @@ -2282,19 +2366,6 @@ class calendar_ical extends calendar_boupdate return false; } - /* - $mozillaACK = $component->getAttributeDefault('X-MOZ-LASTACK', null); - if ($this->productName == 'lightning' && !isset($mozillaACK)) - { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.'()' . - "X-MOZ-LASTACK found\n",3,$this->logfile); - } - return false; - } - */ - if (!empty($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; @@ -2908,6 +2979,31 @@ class calendar_ical extends calendar_boupdate case 'STATUS': // currently no EGroupware event column, but needed as Android uses it to delete single recurrences $event['status'] = $attributes['value']; break; + + // ignore all PROPS, we dont want to store like X-properties or unsupported props + case 'DTSTAMP': + case 'SEQUENCE': + case 'CREATED': + case 'LAST-MODIFIED': + case 'DTSTART': + case 'DTEND': + case 'DURATION': + case 'X-LIC-ERROR': // parse errors from libical, makes no sense to store them + break; + + default: // X- attribute or other by EGroupware unsupported property + //error_log(__METHOD__."() $attributes[name] = ".array2string($attributes)); + // for attributes with multiple values in multiple lines, merge the values + if (isset($event['##'.$attributes['name']])) + { + //error_log(__METHOD__."() taskData['##$attribute[name]'] = ".array2string($taskData['##'.$attribute['name']])); + $attributes['values'] = array_merge( + is_array($event['##'.$attributes['name']]) ? $event['##'.$attributes['name']]['values'] : (array)$event['##'.$attributes['name']], + $attributes['values']); + } + $event['##'.$attributes['name']] = $attributes['params'] || count($attributes['values']) > 1 ? + json_encode($attributes) : $attributes['value']; + break; } } // check if the entry is a birthday diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index 1ff23f3ef3..4c6585eb9e 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -1628,8 +1628,10 @@ ORDER BY cal_user_type, cal_usre_id { foreach ($event['alarm'] as $id => $alarm) { - if (is_numeric($id)) unset($alarm['id']); // unset the temporary id to add the alarm - + if ($alarm['id'] && strpos($alarm['id'], 'cal:'.$cal_id.':') !== 0) + { + unset($alarm['id']); // unset the temporary id to add the alarm + } if(!isset($alarm['offset'])) { $alarm['offset'] = $event['cal_start'] - $alarm['time']; @@ -1784,7 +1786,7 @@ ORDER BY cal_user_type, cal_usre_id /** * Combine status, quantity and role into one value * - * @param string $status + * @param string $status status letter: U, T, A, R * @param int $quantity =1 * @param string $role ='REQ-PARTICIPANT' * @return string @@ -2298,6 +2300,8 @@ ORDER BY cal_user_type, cal_usre_id $this->async->cancel_timer($id); } $alarm['cal_id'] = $cal_id; // we need the back-reference + // add an alarm uid, if none is given + if (empty($alarm['uid']) && class_exists('Horde_Support_Uuid')) $alarm['uid'] = (string)new Horde_Support_Uuid; //error_log(__METHOD__.__LINE__.' Save Alarm for CalID:'.$cal_id.'->'.array2string($alarm).'-->'.$id.'#'.function_backtrace()); // allways store job with the alarm owner as job-owner to get eg. the correct from address if (!$this->async->set_timer($alarm['time'],$id,'calendar.calendar_boupdate.send_alarm',$alarm,$alarm['owner'])) @@ -2855,7 +2859,7 @@ ORDER BY cal_user_type, cal_usre_id /** * Moves a datetime to the beginning of the day within timezone * - * @param egw_time &time the datetime entry + * @param egw_time $time the datetime entry * @param string tz_id timezone * * @return DateTime