diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index 7cb6f3624d..d29486bb9e 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -101,13 +101,14 @@ class calendar_bo 'R' => 'Rejected', 'T' => 'Tentative', 'U' => 'No Response', + 'D' => 'Delegated', 'G' => 'Group invitation', ); /** * @var array recur_types translates MCAL recur-types to verbose labels */ var $recur_types = Array( - MCAL_RECUR_NONE => 'None', + MCAL_RECUR_NONE => 'No recurrence', MCAL_RECUR_DAILY => 'Daily', MCAL_RECUR_WEEKLY => 'Weekly', MCAL_RECUR_MONTHLY_WDAY => 'Monthly (by day)', @@ -166,7 +167,7 @@ class calendar_bo /** * Instance of the categories class * - * @var $categories + * @var categories */ var $categories; @@ -290,9 +291,9 @@ class calendar_bo * filter string filter-name, atm. 'all' or 'hideprivate' * query string pattern so search for, if unset or empty all matching entries are returned (no search) * Please Note: a search never returns repeating events more then once AND does not honor start+end date !!! - * dayswise boolean on True it returns an array with YYYYMMDD strings as keys and an array with events + * daywise boolean on True it returns an array with YYYYMMDD strings as keys and an array with events * (events spanning multiple days are returned each day again (!)) otherwise it returns one array with - * the events (default), not honored in a search ==> always returns an array of events ! + * the events (default), not honored in a search ==> always returns an array of events! * date_format string date-formats: 'ts'=timestamp (default), 'array'=array, or string with format for date * offset boolean/int false (default) to return all entries or integer offset to return only a limited result * enum_recuring boolean if true or not set (default) or daywise is set, each recurence of a recuring events is returned, @@ -399,7 +400,7 @@ class calendar_bo } // date2ts(,true) converts to server time, db2data converts again to user-time $events =& $this->so->search(isset($start) ? $this->date2ts($start,true) : null,isset($end) ? $this->date2ts($end,true) : null, - $users,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$show_rejected,$params['cols'],$params['append']); + $users,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$show_rejected,$params['cols'],$params['append'],$params['cfs']); if (isset($params['cols'])) { @@ -555,7 +556,7 @@ class calendar_bo $old_horizont = $this->config['horizont']; $this->config['horizont'] = $new_horizont; - // create further recurances for all recuring and not yet (at the old horizont) ended events + // create further recurrences for all recurring and not yet (at the old horizont) ended events if (($recuring = $this->so->unfinished_recuring($old_horizont))) { foreach($this->read(array_keys($recuring)) as $cal_id => $event) @@ -576,10 +577,10 @@ class calendar_bo } /** - * set all recurances for an event til the defined horizont $this->config['horizont'] + * set all recurrences for an event until the defined horizont $this->config['horizont'] * * @param array $event - * @param mixed $start=0 minimum start-time for new recurances or !$start = since the start of the event + * @param mixed $start=0 minimum start-time for new recurrences or !$start = since the start of the event */ function set_recurrences($event,$start=0) { @@ -587,11 +588,19 @@ class calendar_bo { $this->debug_message('bocal::set_recurrences(%1,%2)',true,$event,$start); } - // check if the caller gave the participants and if not read them from the DB - if (!isset($event['participants'])) + // check if the caller gave us enough information and if not read it from the DB + if (!isset($event['participants']) || !isset($event['start']) || !isset($event['end'])) { list(,$event_read) = each($this->so->read($event['id'])); - $event['participants'] = $event_read['participants']; + if (!isset($event['participants'])) + { + $event['participants'] = $event_read['participants']; + } + if (!isset($event['start']) || !isset($event['end'])) + { + $event['start'] = $event_read['start']; + $event['end'] = $event_read['end']; + } } if (!$start) $start = $event['start']; @@ -679,11 +688,11 @@ class calendar_bo /** * Reads a calendar-entry * - * @param int/array/string $ids id or array of id's of the entries to read, or string with a single uid + * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid * @param mixed $date=null date to specify a single event of a series * @param boolean $ignore_acl should we ignore the acl, default False for a single id, true for multiple id's * @param string $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in servertime, 'array'=array, or string with date-format - * @return boolean/array event or array of id => event pairs, false if the acl-check went wrong, null if $ids not found + * @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') { @@ -747,9 +756,9 @@ class calendar_bo */ function insert_all_repetitions($event,$start,$end,&$events,$recur_exceptions) { - if ((int) $this->debug >= 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repitions') + if ((int) $this->debug >= 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repetions') { - $this->debug_message('bocal::insert_all_repitions(%1,%2,%3,&$event,%4)',true,$event,$start,$end,$recur_exceptions); + $this->debug_message(__METHOD__.'(%1,%2,%3,&$event,%4)',true,$event,$start,$end,$recur_exceptions); } $start_in = $start; $end_in = $end; @@ -758,9 +767,9 @@ class calendar_bo $event_start_ts = $this->date2ts($event['start']); $event_end_ts = $this->date2ts($event['end']); - if ($this->debug && ((int) $this->debug > 3 || $this->debug == 'insert_all_repetions' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repitions')) + if ($this->debug && ((int) $this->debug > 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repetions')) { - $this->debug_message('bocal::insert_all_repetions(%1,start=%2,end=%3,,%4) starting...',True,$event,$start_in,$end_in,$recur_exceptions); + $this->debug_message(__METHOD__.'(%1,start=%2,end=%3,,%4) starting...',True,$event,$start_in,$end_in,$recur_exceptions); } $id = $event['id']; $event_start_arr = $this->date2array($event['start']); @@ -800,9 +809,9 @@ class calendar_bo if (($have_exception = $search_date_ymd == (int)$this->date2string($exception_ts))) break; } } - if ($this->debug && ((int) $this->debug > 3 || $this->debug == 'insert_all_repetions' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repitions')) + if ($this->debug && ((int) $this->debug > 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repetions')) { - $this->debug_message('bocal::insert_all_repetions(...,%1) checking recur_exceptions[%2] and event[recur_exceptions]=%3 ==> %4',False, + $this->debug_message(__METHOD__.'(...,%1) checking recur_exceptions[%2] and event[recur_exceptions]=%3 ==> %4',False, $recur_exceptions,$search_date_ymd,$event['recur_exception'],$have_exception); } if ($have_exception) @@ -914,18 +923,18 @@ class calendar_bo break; } // switch(recur-type) } // for($date = ...) - if ($this->debug && ((int) $this->debug > 2 || $this->debug == 'insert_all_repetions' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repitions')) + if ($this->debug && ((int) $this->debug > 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_repetions')) { - $this->debug_message('bocal::insert_all_repetions(%1,start=%2,end=%3,events,exections=%4) events=%5',True,$event,$start_in,$end_in,$recur_exceptions,$events); + $this->debug_message(__METHOD__.'(%1,start=%2,end=%3,events,exections=%4) events=%5',True,$event,$start_in,$end_in,$recur_exceptions,$events); } } /** * Adds one repetion of $event for $date_ymd to the $events array, after adjusting its start- and end-time * - * @param $events array in which the event gets inserted - * @param $event array event to insert, it has start- and end-date of the first recurrence, not of $date_ymd - * @param $date_ymd int/string of the date of the event + * @param array $events in which the event gets inserted + * @param array $event event to be inserted; it has start- and end-date of the first recurrence, not of $date_ymd + * @param int|string $date_ymd of the date of the event */ function add_adjusted_event(&$events,$event,$date_ymd) { @@ -1092,13 +1101,13 @@ class calendar_bo /** * Converts several date-types to a timestamp and optionaly converts user- to server-time * - * @param $date mixed date to convert, should be one of the following types + * @param mixed $date date to convert, should be one of the following types * string (!) in form YYYYMMDD or iso8601 YYYY-MM-DDThh:mm:ss or YYYYMMDDThhmmss * int already a timestamp * array with keys 'second', 'minute', 'hour', 'day' or 'mday' (depricated !), 'month' and 'year' - * @param $user2server_time boolean conversation between user- and server-time default False == Off + * @param boolean $user2server_time conversation between user- and server-time; default false == Off */ - function date2ts($date,$user2server=False) + function date2ts($date,$user2server=false) { $date_in = $date; @@ -1173,11 +1182,11 @@ class calendar_bo /** * Converts a date to an array and optionaly converts server- to user-time * - * @param $date mixed date to convert - * @param $server2user_time boolean conversation between user- and server-time default False == Off + * @param mixed $date date to convert + * @param boolean $server2user_time=false conversation between user- and server-time; default false == Off * @return array with keys 'second', 'minute', 'hour', 'day', 'month', 'year', 'raw' (timestamp) and 'full' (Ymd-string) */ - function date2array($date,$server2user=False) + function date2array($date,$server2user=false) { $date_called = $date; @@ -1254,7 +1263,7 @@ class calendar_bo * Formats a date given as timestamp or array * * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert - * @param string/boolean $format='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only + * @param string|boolean $format='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only * @return string the formated date (incl. time) */ function format_date($date,$format='') @@ -1288,13 +1297,13 @@ class calendar_bo * * The parameters get formated depending on their type. ACL-values need a ACL_TYPE_IDENTIFER prefix. * - * @param $msg string message with parameters/variables like lang(), eg. '%1' - * @param $backtrace include a function-backtrace, default True=On + * @param string $msg message with parameters/variables like lang(), eg. '%1' + * @param boolean $backtrace=true include a function-backtrace, default true=On * should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!! - * @param $param mixed a variable number of parameters, to be inserted in $msg + * @param mixed $param a variable number of parameters, to be inserted in $msg * arrays get serialized with print_r() ! */ - function debug_message($msg,$backtrace=True) + function debug_message($msg,$backtrace=true) { static $acl2string = array( 0 => 'ACL-UNKNOWN', @@ -1473,7 +1482,7 @@ class calendar_bo /** * Converts a participant into a (readable) user- or resource-name * - * @param $id string|int id of user or resource + * @param string|int $id id of user or resource * @return string with name */ function participant_name($id,$use_type=false) @@ -1534,6 +1543,9 @@ class calendar_bo case 'U': // no response = unknown $status = html::image('calendar','cnr-pending',$this->verbose_status[$status]); break; + case 'D': // delegated + $status = html::image('calendar','forward',$this->verbose_status[$status]); + break; case 'G': // group invitation // Todo: Image, seems not to be used $status = '('.$this->verbose_status[$status].')'; @@ -1571,8 +1583,8 @@ class calendar_bo /** * Converts category string of an event into array of (readable) category-names * - * @param $category string cat-id (multiple id's commaseparated) - * @param $color int color of the category, if multiple cats, the color of the last one with color is returned + * @param string $category cat-id (multiple id's commaseparated) + * @param int $color color of the category, if multiple cats, the color of the last one with color is returned * @return array with id / names */ function categories($category,&$color) @@ -1656,7 +1668,7 @@ class calendar_bo } /** - * Convert the recure-information of an event, into a human readable string + * Convert the recurrence-information of an event, into a human readable string * * @param array $event * @return string @@ -1708,7 +1720,7 @@ class calendar_bo * * The holidays get cached in the session (performance), so changes in holidays or birthdays do NOT affect a current session!!! * - * @param integer $year=0 year, defaults to 0 = current year + * @param int $year=0 year, defaults to 0 = current year * @return array indexed with Ymd of array of holidays. A holiday is an array with the following fields: * index: numerical unique id * locale: string, 2-char short for the nation @@ -1775,8 +1787,8 @@ class calendar_bo * * Is called as hook to participate in the linking * - * @param int/array $entry int cal_id or array with event - * @param string/boolean string with title, null if not found or false if not read perms + * @param int|array $entry int cal_id or array with event + * @param string|boolean string with title, null if not found or false if not read perms */ function link_title($event) { @@ -1797,7 +1809,7 @@ class calendar_bo * Is called as hook to participate in the linking * * @param string $pattern pattern to search - * @return array with pm_id - title pairs of the matching entries + * @return array with cal_id - title pairs of the matching entries */ function link_query($pattern) { @@ -1883,7 +1895,7 @@ class calendar_bo /** * Get the freebusy URL of a user * - * @param int/string $user account_id or account_lid + * @param int|string $user account_id or account_lid * @param string $pw=null password */ static function freebusy_url($user,$pw=null) @@ -1900,13 +1912,14 @@ class calendar_bo * Check if the event is the whole day * * @param event + * @param boolean $server2user_time=false conversation between user- and server-time; default false == Off * @return boolean true for whole day events */ - function isWholeDay($event) + function isWholeDay($event, $server2user=false) { // check if the event is the whole day - $start = $this->date2array($event['start']); - $end = $this->date2array($event['end']); + $start = $this->date2array($event['start'], $server2user); + $end = $this->date2array($event['end'], $server2user); $result = (!$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59); return $result; diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 3dfe41a76e..2909986aeb 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -20,6 +20,7 @@ define('MSG_TENTATIVE',4); define('MSG_ACCEPTED',5); define('MSG_ALARM',6); define('MSG_DISINVITE',7); +define('MSG_DELEGATED',8); /** * Class to access AND manipulate all calendar data (business object) @@ -51,6 +52,13 @@ class calendar_boupdate extends calendar_bo */ var $debug; + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + /** * @var string|boolean $log_file filename to enable the login or false for no update-logging */ @@ -97,29 +105,35 @@ class calendar_boupdate extends calendar_bo return false; } - if (!$event['id']) // some defaults for new entries + if (($new_event = !$event['id'])) // some defaults for new entries { // if no owner given, set user to owner if (!$event['owner']) $event['owner'] = $this->user; // set owner as participant if none is given if (!is_array($event['participants']) || !count($event['participants'])) { - $event['participants'] = array($event['owner'] => 'U'); - } - // set the status of the current user to 'A' = accepted - if (isset($event['participants'][$this->user]) && $event['participants'][$this->user][0] != 'A') - { - $event['participants'][$this->user][0] = 'A'; + $status = $event['owner'] == $this->user ? 'A' : 'U'; + $status = calendar_so::combine_status($status, 1, 'CHAIR'); + $event['participants'] = array($event['owner'] => $status); } } + // check if user has the permission to update / create the event - if (!$ignore_acl && ($event['id'] && !$this->check_perms(EGW_ACL_EDIT,$event['id']) || - !$event['id'] && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner'])) && + if (!$ignore_acl && (!$new_event && !$this->check_perms(EGW_ACL_EDIT,$event['id']) || + $new_event && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner'])) && !$this->check_perms(EGW_ACL_ADD,0,$event['owner'])) { return false; } + if (!$new_event) + { + $old_event = $this->read((int)$event['id'],null,$ignore_acl); + // if no participants are set, set them from the old event, as we might need them to update recuring events + if (!isset($event['participants'])) $event['participants'] = $old_event['participants']; + //echo "old $event[id]="; _debug_array($old_event); + } + // check for conflicts only happens !$ignore_conflicts AND if start + end date are given if (!$ignore_conflicts && !$event['non_blocking'] && isset($event['start']) && isset($event['end'])) { @@ -250,14 +264,6 @@ class calendar_boupdate extends calendar_bo $event['modified'] = $this->now_su; // we are still in user-time $event['modifier'] = $GLOBALS['egw_info']['user']['account_id']; } - if (!($new_event = !(int)$event['id'])) - { - $old_event = $this->read((int)$event['id'],null,$ignore_acl); - // if no participants are set, set them from the old event, as we might need them to update recuring events - if (!isset($event['participants'])) $event['participants'] = $old_event['participants']; - //echo "old $event[id]="; _debug_array($old_event); - } - //echo "saving $event[id]="; _debug_array($event); $event2save = $event; @@ -359,7 +365,7 @@ class calendar_boupdate extends calendar_bo // the following switch falls through all cases, as each included the following too // - $msg_is_response = $msg_type == MSG_REJECTED || $msg_type == MSG_ACCEPTED || $msg_type == MSG_TENTATIVE; + $msg_is_response = $msg_type == MSG_REJECTED || $msg_type == MSG_ACCEPTED || $msg_type == MSG_TENTATIVE || $msg_type == MSG_DELEGATED; switch($ru = $part_prefs['calendar']['receive_updates']) { @@ -485,6 +491,12 @@ class calendar_boupdate extends calendar_bo $msgtype = '"calendar";'; $method = 'REPLY'; break; + case MSG_DELEGATED: + $action = lang('Delegated'); + $msg = 'Response'; + $msgtype = '"calendar";'; + $method = 'REPLY'; + break; case MSG_ALARM: $action = lang('Alarm'); $msg = 'Alarm'; @@ -738,7 +750,7 @@ class calendar_boupdate extends calendar_bo */ function check_status_perms($uid,$event) { - if ($uid[0] == 'c' || $uid['0'] == 'e') // for contact we use the owner of the event + if ($uid[0] == 'c' || $uid[0] == 'e') // for contact we use the owner of the event { if (!is_array($event) && !($event = $this->read($event))) return false; @@ -781,6 +793,7 @@ class calendar_boupdate extends calendar_bo 'R' => MSG_REJECTED, 'T' => MSG_TENTATIVE, 'A' => MSG_ACCEPTED, + 'D' => MSG_DELEGATED, ); if (isset($status2msg[$status])) { @@ -802,8 +815,6 @@ class calendar_boupdate extends calendar_bo */ function delete($cal_id,$recur_date=0,$ignore_acl=false) { - $event = $this->read($cal_id,$recur_date); - if (!($event = $this->read($cal_id,$recur_date)) || !$ignore_acl && !$this->check_perms(EGW_ACL_DELETE,$event)) { @@ -828,7 +839,7 @@ class calendar_boupdate extends calendar_bo } if ($event['reference']) { - // evtl. delete recur_exception $event['recurrence'] from event with cal_id=$event['reference'] + // evtl. delete recur_exception $event['reference'] from event with cal_id=$event['reference'] } return true; } @@ -1138,110 +1149,597 @@ class calendar_boupdate extends calendar_bo * Try to find a matching db entry * * @param array $event the vCalendar data we try to find - * @param boolean $relax=false if asked to relax, we only match against some key fields - * @return the calendar_id of the matching entry or false (if none matches) + * @param string filter='exact' exact -> find the matching entry + * check -> check (consitency) for identical matches + * relax -> be more tolerant + * master -> try to find a releated series master + * @return array calendar_ids of matching entries */ - function find_event($event, $relax=false) + function find_event($event, $filter='exact') { + $matchingEvents = array(); $query = array(); - if (isset($event['start'])) + $recur_date = 0; + + if ($this->log) { - $query[] = 'cal_start='.$event['start']; - } - if (isset($event['end'])) - { - $query[] = 'cal_end='.$event['end']; + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($filter)[EVENT]:" . array2string($event)); } - foreach (array('title', 'location', - 'public', 'non_blocking', 'category') as $key) + if ($filter == 'master') { - if (!empty($event[$key])) $query['cal_'.$key] = $event[$key]; - } - - if ($event['uid'] && ($uidmatch = $this->read($event['uid']))) - { - if ($event['reference']) + if (isset($event['reference'])) { - // Let's try to find a real exception first - $query['cal_uid'] = $event['uid']; - $query['cal_reference'] = $event['reference']; - - if ($foundEvents = parent::search(array( - 'query' => $query, - ))) - { - if(is_array($foundEvents)) - { - $event = array_shift($foundEvents); - return $event['id']; - } - } - // Let's try the "status only" (pseudo) exceptions now - if (($egw_event = $this->read($uidmatch['id'], $event['reference']))) - { - // Do we work with a pseudo exception here? - $match = true; - foreach (array('start', 'end', 'title', 'priority', - 'location', 'public', 'non_blocking') as $key) - { - if (isset($event[$key]) - && $event[$key] != $egw_event[$key]) - { - $match = false; - break; - } - } - if ($match && is_array($event['participants'])) - { - foreach ($event['participants'] as $attendee => $status) - { - if (!isset($egw_event['participants'][$attendee]) - || $egw_event['participants'][$attendee] != $status) - { - $match = false; - break; - } - else - { - unset($egw_event['participants'][$attendee]); - } - } - if ($match && !empty($egw_event['participants'])) $match = false; - } - if ($match) return ($uidmatch['id'] . ':' . $event['reference']); - - return false; // We need to create a new pseudo exception - } + $recur_date = $event['reference']; } - else + elseif (isset($event['start'])) { - return $uidmatch['id']; + $recur_date = $event['start']; } } - if ($event['id'] && ($found = $this->read($event['id']))) + if ($event['id']) { - // We only do a simple consistency check - if ($found['title'] == $event['title'] - && $found['start'] == $event['start'] - && $found['end'] == $event['end']) + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['id'] . ")[EventID]"); + } + if (($egwEvent = $this->read($event['id'], $recur_date, false, 'server'))) + { + if ($this->log) { - return $found['id']; + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '()[FOUND]:' . array2string($egwEvent)); } + // Just a simple consistency check + if ($filter == 'exact' || + $filter == 'master' && $egwEvent['recur_type'] != MCAL_RECUR_NONE || + $filter != 'master' && strpos($egwEvent['title'], $event['title']) === 0) + { + $retval = $egwEvent['id']; + if ($egwEvent['recur_type'] != MCAL_RECUR_NONE && + $event['recur_type'] == MCAL_RECUR_NONE && $event['reference'] != 0) + { + $retval .= ':' . (int)$event['reference']; + } + $matchingEvents[] = $retval; + return $matchingEvents; + } + } + if ($filter == 'exact') return array(); } unset($event['id']); - if($foundEvents = parent::search(array( - 'query' => $query, - ))) + if ($filter == 'master') { - if(is_array($foundEvents)) + $query[] = 'recur_type!='. MCAL_RECUR_NONE; + $query['cal_reference'] = 0; + } + + // only query calendars of users, we have READ-grants from + $users = array(); + foreach(array_keys($this->grants) as $user) + { + $user = trim($user); + if ($this->check_perms(EGW_ACL_READ|EGW_ACL_READ_FOR_PARTICIPANTS|EGW_ACL_FREEBUSY,0,$user)) { - $event = array_shift($foundEvents); - return $event['id']; + if ($user && !in_array($user,$users)) // already added? + { + $users[] = $user; + } + } + elseif ($GLOBALS['egw']->accounts->get_type($user) != 'g') + { + continue; // for non-groups (eg. users), we stop here if we have no read-rights + } + // the further code is only for real users + if (!is_numeric($user)) continue; + + // for groups we have to include the members + if ($GLOBALS['egw']->accounts->get_type($user) == 'g') + { + $members = $GLOBALS['egw']->accounts->member($user); + if (is_array($members)) + { + foreach($members as $member) + { + // use only members which gave the user a read-grant + if (!in_array($member['account_id'],$users) && + $this->check_perms(EGW_ACL_READ|EGW_ACL_FREEBUSY,0,$member['account_id'])) + { + $users[] = $member['account_id']; + } + } + } + } + else // for users we have to include all the memberships, to get the group-events + { + $memberships = $GLOBALS['egw']->accounts->membership($user); + if (is_array($memberships)) + { + foreach($memberships as $group) + { + if (!in_array($group['account_id'],$users)) + { + $users[] = $group['account_id']; + } + } + } } } - return false; + if ($filter != 'master' && ($filter != 'exact' || empty($event['uid']))) + { + if (isset($event['whole_day']) && $event['whole_day']) + { + if ($filter == 'relax') + { + $delta = 1800; + } + else + { + $delta = 60; + } + + // check length with some tolerance + $length = $event['end'] - $event['start'] - $delta; + $query[] = ('(cal_end-cal_start)>' . $length); + $length += 2 * $delta; + $query[] = ('(cal_end-cal_start)<' . $length); + $query[] = ('cal_start>' . ($event['start'] - 86400)); + $query[] = ('cal_start<' . ($event['start'] + 86400)); + } + elseif (isset($event['start'])) + { + if ($filter == 'relax') + { + $query[] = ('cal_start>' . ($event['start'] - 3600)); + $query[] = ('cal_start<' . ($event['start'] + 3600)); + } + else + { + // we accept a tiny tolerance + $query[] = ('cal_start>' . ($event['start'] - 2)); + $query[] = ('cal_start<' . ($event['start'] + 2)); + } + } + $matchFields = array('priority', 'public', 'non_blocking', 'reference'); + foreach ($matchFields as $key) + { + if (isset($event[$key])) $query['cal_'.$key] = $event[$key]; + } + } + if (!empty($event['uid'])) + { + $query['cal_uid'] = $event['uid']; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['uid'] . ')[EventUID]'); + } + if ($filter != 'master' && isset($event['reference'])) + { + $query['cal_reference'] = $event['reference']; + } + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[QUERY]: ' . array2string($query)); + } + if (!count($users) || !($foundEvents = + $this->so->search(null, null, $users, 0, 'all', $query))) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[NO MATCH]'); + } + return $matchingEvents; + } + + $pseudos = array(); + + foreach($foundEvents as $egwEvent) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[FOUND]: ' . array2string($egwEvent)); + } + if (in_array($egwEvent['id'], $matchingEvents)) continue; + + if (in_array($filter, array('exact', 'master')) && !empty($event['uid'])) + { + $matchingEvents[] = $egwEvent['id']; // UID found + if ($filter = 'master') break; + continue; + } + + // check times + if ($filter != 'relax') + { + if (isset($event['whole_day'])&& $event['whole_day']) + { + if (!$this->isWholeDay($egwEvent, true)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() egwEvent is not a whole-day event!'); + } + continue; + } + } + elseif ($filter != 'master') + { + if (abs($event['end'] - $egwEvent['end']) >= 120) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() egwEvent length does not match!'); + } + continue; + } + } + } + + // check for real match + $matchFields = array('title'); + switch ($filter) + { + case 'master': + break; + case 'relax': + $matchFields[] = 'location'; + default: + $matchFields[] = 'description'; + } + foreach ($matchFields as $key) + { + if (!empty($event[$key]) && (empty($egwEvent[$key]) + || strpos($egwEvent[$key], $event[$key]) !== 0)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() event[$key] differ: '" . $event[$key] . + "' <> '" . $egwEvent[$key]) . "'"; + } + continue 2; // next foundEvent + } + } + + if ($filter != 'master' && is_array($event['category'])) + { + // check categories + $egwCategories = explode(',', $egwEvent['category']); + foreach ($egwCategories as $cat_id) + { + if ($this->categories->check_perms(EGW_ACL_READ, $cat_id) && + !in_array($cat_id, $event['category'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() egwEvent category $cat_id is missing!"); + } + continue 2; + } + } + $newCategories = array_diff($event['category'], $egwCategories); + if (!empty($newCategories)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() event has additional categories:' . array2string($newCategories)); + } + continue; + } + } + + if ($filter != 'relax' && $filter != 'master') + { + // check participants + if (is_array($event['participants'])) + { + foreach ($event['participants'] as $attendee => $status) + { + if (!isset($egwEvent['participants'][$attendee]) && + $attendee != $egwEvent['owner']) // || + //(!$relax && $egw_event['participants'][$attendee] != $status)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() additional event['participants']: $attendee"); + } + continue 2; + } + else + { + unset($egwEvent['participants'][$attendee]); + } + } + // ORGANIZER is maybe missing + unset($egwEvent['participants'][$egwEvent['owner']]); + if (!empty($egwEvent['participants'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() missing event[participants]: ' . + array2string($egwEvent['participants'])); + } + continue; + } + } + } + + if ($filter != 'master') + { + if ($event['recur_type'] == MCAL_RECUR_NONE) + { + if ($egwEvent['recur_type'] != MCAL_RECUR_NONE) + { + // We found a pseudo Exception + $start = $this->date2ts($event['start'], true); + $pseudos[] = $egwEvent['id'] . ':' . $start; + continue; + } + } + elseif ($filter != 'relax') + { + // check exceptions + // $exceptions[$remote_ts] = $egw_ts + $exceptions = $this->so->get_recurrence_exceptions($egwEvent); + $exceptions = array_merge($egwEvent['recur_exception'], $exceptions); + if (is_array($event['recur_exception'])) + { + foreach ($event['recur_exception'] as $key => $day) + { + if (isset($exceptions[$day])) + { + unset($exceptions[$day]); + } + else + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() additional event['recur_exception']: $day"); + } + continue 2; + } + } + if (!empty($exceptions)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() missing event[recur_exception]: ' . + array2string($event['recur_exception'])); + } + continue; + } + } + + // check recurrence information + foreach (array('recur_type', 'recur_interval', 'recur_enddate') as $key) + { + if (isset($event[$key]) + && $event[$key] != $egwEvent[$key]) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() events[$key] differ: " . $event[$key] . + ' <> ' . $egwEvent[$key]); + } + continue 2; + } + } + } + } + $matchingEvents[] = $egwEvent['id']; // exact match + if ($filter = 'master') break; + } + // append pseudos as last entries + $matchingEvents = array_merge($matchingEvents, $pseudos); + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[MATCHES]:' . array2string($matchingEvents)); + } + return $matchingEvents; } + + /** + * classifies an incoming event from the eGW point-of-view + * + * exceptions: unlike other calendar apps eGW does not create an event exception + * if just the participant state changes - therefore we have to distinguish between + * real exceptions and status only exceptions + * + * @param array $event the event to check + * + * @return array + * type => + * SINGLE a single event + * SERIES-MASTER the series master + * SERIES-EXCEPTION event is a real exception + * SERIES-PSEUDO-EXCEPTION event is a status only exception + * SERIES-EXCEPTION-PROPAGATE event was a status only exception in the past and is now a real exception + * stored_event => if event already exists in the database array with event data or false + * master_event => for event type SERIES-EXCEPTION, SERIES-PSEUDO-EXCEPTION or SERIES-EXCEPTION-PROPAGATE + * the corresponding series master event array + * NOTE: this param is false if event is of type SERIES-MASTER + */ + function get_event_info($event) + { + $type = 'SINGLE'; // default + $master_event = false; //default + $stored_event = false; + $recurrence_event = false; + $wasPseudo = false; + + if (($foundEvents = $this->find_event($event, 'exact'))) + { + // We found the exact match + $eventID = array_shift($foundEvents); + if (strstr($eventID, ':')) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; + $wasPseudo = true; + list($eventID, $recur_date) = explode(':', $eventID); + $recur_date = $this->date2usertime($recur_date); + $stored_event = $this->read($eventID, $recur_date, false, 'server'); + $master_event = $this->read($eventID, 0, false, 'server'); + $recurrence_event = $stored_event; + } + else + { + $stored_event = $this->read($eventID, 0); + } + if (!empty($stored_event['uid']) && empty($event['uid'])) + { + $event['uid'] = $stored_event['uid']; // restore the UID if it was not delivered + } + } + + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + $type = 'SERIES-MASTER'; + } + + if ($type == 'SINGLE' && + ($foundEvents = $this->find_event($event, 'master'))) + { + // SINGLE, SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS + foreach ($foundEvents as $eventID) + { + // Let's try to find a related series + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "()[MASTER]: $eventID"); + } + if (($master_event = $this->read($eventID, 0))) + { + if (isset($stored_event['id']) && $master_event['id'] != $stored_event['id']) + { + $type = 'SERIES-EXCEPTION'; // this is an existing exception + break; + } + elseif (isset($event['reference']) && + in_array($event['reference'], $master_event['recur_exception'])) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; // could also be a real one + $recurrence_event = $master_event; + $recurrence_event['start'] = $event['reference']; + $recurrence_event['end'] -= $master_event['start'] - $event['reference']; + break; + } + elseif (in_array($event['start'], $master_event['recur_exception'])) + { + $type='SERIES-PSEUDO-EXCEPTION'; // new pseudo exception? + $recurrence_event = $master_event; + $recurrence_event['start'] = $event['start']; + $recurrence_event['end'] -= $master_event['start'] - $event['start']; + break; + } + else + { + // try to find a suitable pseudo exception date + $recur_date = $this->date2usertime($event['start']); + $egwEvent = $this->read($eventID, $recur_date, false, 'server'); + if ($event['start'] == $egwEvent['start']) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; // let's try a pseudo exception + $recurrence_event = $master_event; + $recurrence_event['start'] = $event['start']; + $recurrence_event['end'] -= $master_event['start'] - $event['start']; + break; + + } + $recur_date = $this->date2usertime($event['reference']); + $egwEvent = $this->read($eventID,$recur_date , false, 'server'); + if (isset($event['reference']) && $event['reference'] == $egwEvent['start']) + { + $type = 'SERIES-EXCEPTION-PROPAGATE'; + if ($stored_event) + { + unset($stored_event['id']); // signal the true exception + $stored_event['recur_type'] = MCAL_RECUR_NONE; + } + break; + } + } + } + } + } + + // check pseudo exception propagation + if ($recurrence_event) + { + // default if we cannot find a proof for a fundamental change + // the recurrence_event is the master event with start and end adjusted to the recurrence + // check for changed data + foreach (array('start','end','uid','title','location','description', + 'priority','public','special','non_blocking') as $key) + { + if (!empty($event[$key]) && $recurrence_event[$key] != $event[$key]) + { + if ($wasPseudo) + { + // We started with a pseudo exception + $type = 'SERIES-EXCEPTION-PROPAGATE'; + } + else + { + $type = 'SERIES-EXCEPTION'; + } + + if ($stored_event) + { + unset($stored_event['id']); // signal the true exception + $stored_event['recur_type'] = MCAL_RECUR_NONE; + } + break; + } + } + // the event id here is always the id of the master event + // unset it to prevent confusion of stored event and master event + unset($event['id']); + } + + // check ACL + if (is_array($master_event)) + { + $acl_edit = $this->check_perms(EGW_ACL_EDIT, $master_event['id']); + } + else + { + if (is_array($stored_event)) + { + $acl_edit = $this->check_perms(EGW_ACL_EDIT, $stored_event['id']); + } + else + { + $acl_edit = true; // new event + } + } + + return array( + 'type' => $type, + 'acl_edit' => $acl_edit, + 'stored_event' => $stored_event, + 'master_event' => $master_event, + ); + } } \ No newline at end of file diff --git a/calendar/inc/class.calendar_groupdav.inc.php b/calendar/inc/class.calendar_groupdav.inc.php index 964464d7c8..79bf6a7487 100644 --- a/calendar/inc/class.calendar_groupdav.inc.php +++ b/calendar/inc/class.calendar_groupdav.inc.php @@ -33,9 +33,25 @@ class calendar_groupdav extends groupdav_handler //'RDATE' => 'cal_start', //'EXRULE' //'EXDATE' - 'RECURRENCE-ID' => 'cal_reference', + //'RECURRENCE-ID' => 'cal_reference', ); + /** + * Does client understand exceptions to be included in VCALENDAR component of series master sharing its UID + * + * That also means no EXDATE for these exceptions! + * + * Setting it to false, should give the old behavior used in 1.6 (hopefully) no client needs that. + * + * @var boolean + */ + var $client_shared_uid_exceptions = true; + + /** + * Are we using id or uid for the path/url + */ + const PATH_ATTRIBUTE = 'id'; + /** * Constructor * @@ -50,8 +66,6 @@ class calendar_groupdav extends groupdav_handler $this->bo = new calendar_boupdate(); } - const PATH_ATTRIBUTE = 'id'; - /** * Create the path for an event * @@ -84,11 +98,13 @@ class calendar_groupdav extends groupdav_handler */ function propfind($path,$options,&$files,$user,$id='') { - if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user,$id)"); - //error_log(__METHOD__."($path,".array2string($options).",,$user,$id)");//njv: + if ($this->debug) + { + error_log(__METHOD__."($path,".array2string($options).",,$user,$id)"); + $starttime = microtime(true); + } + // ToDo: add parameter to only return id & etag - //error_log( __FILE__ . __METHOD__ ." :$user ". print_r($options,true)); - $st = microtime(true); $cal_filters = array( 'users' => $user, 'start' => time()-100*24*3600, // default one month back -30 breaks all sync recurrences @@ -97,13 +113,23 @@ class calendar_groupdav extends groupdav_handler 'daywise' => false, 'date_format' => 'server', ); - if ($this->debug > 1) error_log(__METHOD__."($path,,,$user,$id) cal_filters=".array2string($cal_filters)); - //error_log(__METHOD__."($path,,,$user,$id) cal_filters=".array2string($cal_filters));//njv + /* + if ($this->client_shared_uid_exceptions) + { + $cal_filters['query']['cal_reference'] = 0; + } + */ // process REPORT filters or multiget href's if (($id || $options['root']['name'] != 'propfind') && !$this->_report_filters($options,$cal_filters,$id)) { return false; } + if ($this->debug > 1) + { + error_log(__METHOD__."($path,,,$user,$id) cal_filters=". + array2string($cal_filters)); + } + // check if we have to return the full calendar data or just the etag's if (!($calendar_data = $options['props'] == 'all' && $options['root']['ns'] == groupdav::CALDAV) && is_array($options['props'])) { @@ -116,11 +142,28 @@ class calendar_groupdav extends groupdav_handler } } } - //error_log(__FILE__ . __METHOD__ ."Filters:" .print_r($cal_filters,true)); - if (($events = $this->bo->search($cal_filters))) + $events =& $this->bo->search($cal_filters); + if ($events) { - foreach($events as $event) + // get all max user modified times at once + foreach($events as $k => &$event) { + if ($this->client_shared_uid_exceptions && + $event['reference'] && + ($master = $this->bo->read($event['reference'], 0, false, 'server')) && + array_search($event['reference'], $master['recur_exception']) !== false) + { + // this exception will be handled with the series master + unset($events[$k]); + continue; + } + $ids[] = $event['id']; + } + $max_user_modified = $this->bo->so->max_user_modified($ids); + + foreach($events as &$event) + { + $event['max_user_modified'] = $max_user_modified[$event['id']]; //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'])); $props = array( HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($event)), @@ -133,8 +176,7 @@ class calendar_groupdav extends groupdav_handler //error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data"); if ($calendar_data) { - if (is_null($handler)) $handler = $this->_get_handler(); - $content = $handler->exportVCal(array($event),'2.0','PUBLISH'); + $content = $this->iCal($event); $props[] = HTTP_WebDAV_Server::mkprop('getcontentlength',bytes($content)); $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content); } @@ -148,8 +190,11 @@ class calendar_groupdav extends groupdav_handler ); } } - $end = microtime(true) - $st; - if ($this->debug) error_log(__FILE__ . __METHOD__ . "Function took : $end"); + if ($this->debug) + { + error_log(__METHOD__."($path) took ".(microtime(true) - $starttime). + ' to return '.count($files['files']).' items'); + } return true; } @@ -175,29 +220,29 @@ class calendar_groupdav extends groupdav_handler switch($filter['name']) { case 'comp-filter': - if ($this->debug > 1) error_log(__METHOD__."($path,...) comp-filter='{$filter['attrs']['name']}'"); + if ($this->debug > 1) error_log(__METHOD__."($options[path],...) comp-filter='{$filter['attrs']['name']}'"); switch($filter['attrs']['name']) { case 'VTODO': return false; // return nothing for now, todo: check if we can pass it on to the infolog handler // todos are handled by the infolog handler - $infolog_handler = new groupdav_infolog(); - return $infolog_handler->propfind($path,$options,$files,$user,$method); + //$infolog_handler = new groupdav_infolog(); + //return $infolog_handler->propfind($path,$options,$files,$user,$method); case 'VCALENDAR': case 'VEVENT': break; // that's our default anyway } break; case 'prop-filter': - if ($this->debug > 1) error_log(__METHOD__."($path,...) prop-filter='{$filter['attrs']['name']}'"); + if ($this->debug > 1) error_log(__METHOD__."($options[path],...) prop-filter='{$filter['attrs']['name']}'"); $prop_filter = $filter['attrs']['name']; break; case 'text-match': - if ($this->debug > 1) error_log(__METHOD__."($path,...) text-match: $prop_filter='{$filter['data']}'"); + if ($this->debug > 1) error_log(__METHOD__."($options[path],...) text-match: $prop_filter='{$filter['data']}'"); if (!isset($this->filter_prop2cal[strtoupper($prop_filter)])) { - if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user) unknown property '$prop_filter' --> ignored"); + if ($this->debug) error_log(__METHOD__."($options[path],".array2string($options).",...) unknown property '$prop_filter' --> ignored"); } else { @@ -206,15 +251,15 @@ class calendar_groupdav extends groupdav_handler unset($prop_filter); break; case 'param-filter': - if ($this->debug) error_log(__METHOD__."($path,...) param-filter='{$filter['attrs']['name']}' not (yet) implemented!"); + if ($this->debug) error_log(__METHOD__."($options[path],...) param-filter='{$filter['attrs']['name']}' not (yet) implemented!"); break; case 'time-range': - if ($this->debug > 1) error_log(__FILE__ . __METHOD__."($path,...) time-range={$filter['attrs']['start']}-{$filter['attrs']['end']}"); + if ($this->debug > 1) error_log(__FILE__ . __METHOD__."($options[path],...) time-range={$filter['attrs']['start']}-{$filter['attrs']['end']}"); $cal_filters['start'] = $filter['attrs']['start']; $cal_filters['end'] = $filter['attrs']['end']; break; default: - if ($this->debug) error_log(__METHOD__."($path,".array2string($options).",,$user) unknown filter --> ignored"); + if ($this->debug) error_log(__METHOD__."($options[path],".array2string($options).",...) unknown filter --> ignored"); break; } } @@ -262,7 +307,7 @@ class calendar_groupdav extends groupdav_handler $cal_filters['query'][] = 'egw_cal.cal_id IN ('.implode(',',array_map(create_function('$n','return (int)$n;'),$ids)).')'; } - if ($this->debug > 1) error_log(__FILE__ . __METHOD__ ."($path,,,$user,$id) calendar-multiget: ids=".implode(',',$ids)); + if ($this->debug > 1) error_log(__FILE__ . __METHOD__ ."($options[path],...,$id) calendar-multiget: ids=".implode(',',$ids)); } return true; } @@ -280,14 +325,103 @@ class calendar_groupdav extends groupdav_handler { return $event; } - $handler = $this->_get_handler(); - $options['data'] = $handler->exportVCal(array($event),'2.0','PUBLISH'); + $options['data'] = $this->iCal($event); $options['mimetype'] = 'text/calendar; charset=utf-8'; header('Content-Encoding: identity'); header('ETag: '.$this->get_etag($event)); return true; } + /** + * Generate an iCal for the given event + * + * Taking into account virtual an real exceptions for recuring events + * + * @param array $event + * @return string + */ + private function iCal(array $event) + { + static $handler = null; + if (is_null($handler)) $handler = $this->_get_handler(); + + $events = array($event); + + // for recuring events we have to add the exceptions + if ($this->client_shared_uid_exceptions && $event['recur_type'] && !empty($event['uid'])) + { + $events =& self::get_series($event['uid'],$this->bo); + } + elseif(!$this->client_shared_uid_exceptions && $event['reference']) + { + $events[0]['uid'] .= '-'.$event['id']; // force a different uid + } + return $handler->exportVCal($events,'2.0','PUBLISH'); + } + + /** + * Get array with events of a series identified by its UID (master and all exceptions) + * + * 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 + * @return array + */ + private static function &get_series($uid,calendar_bo $bo=null) + { + if (is_null($bo)) $bo = new calendar_bopdate(); + + if (!($masterId = array_shift($bo->find_event(array('uid' => $uid), 'master'))) + || !($master = $bo->read($masterId, 0, false, 'server'))) + { + return array(); // should never happen + } + + $exceptions = $master['recur_exception']; + + $events =& $bo->search(array( + 'query' => array('cal_uid' => $uid), + 'daywise' => false, + 'date_format' => 'server', + )); + $events = array_merge(array($master), $events); + foreach($events as $k => &$recurrence) + { + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // "($uid)[$k]:" . array2string($recurrence)); + if (!$k) continue; // nothing to change + + if ($recurrence['id'] != $master['id']) // real exception + { + //error_log('real exception: '.array2string($recurrence)); + // remove from masters recur_exception, as exception is include + // at least Lightning "understands" EXDATE as exception from what's included + // in the whole resource / VCALENDAR component + // not removing it causes Lightning to remove the exception itself + if (($e = array_search($recurrence['reference'],$exceptions)) !== false) + { + unset($exceptions[$e]); + } + continue; // nothing to change + } + // now we need to check if this recurrence is an exception + if ($master['participants'] == $recurrence['participants']) + { + //error_log('NO exception: '.array2string($recurrence)); + unset($events[$k]); // no exception --> remove it + continue; + } + // this is a virtual exception now (no extra event/cal_id in DB) + //error_log('virtual exception: '.array2string($recurrence)); + $recurrence['reference'] = $recurrence['start']; + $recurrence['recur_type'] = MCAL_RECUR_NONE; // is set, as this is a copy of the master + // not for included exceptions (Lightning): $master['recur_exception'][] = $recurrence['start']; + } + $events[0]['recur_exception'] = $exceptions; + return $events; + } + /** * Handle put request for an event * @@ -298,7 +432,7 @@ class calendar_groupdav extends groupdav_handler */ function put(&$options,$id,$user=null) { - if($this->debug) error_log(__METHOD__."($id, $user)".print_r($options,true)); + if ($this->debug) error_log(__METHOD__."($id, $user)".print_r($options,true)); $return_no_access=true; // as handled by importVCal anyway and allows it to set the status for participants $event = $this->_common_get_put_delete('PUT',$options,$id,$return_no_access); @@ -308,6 +442,12 @@ class calendar_groupdav extends groupdav_handler return $event; } $handler = $this->_get_handler(); + + if (!is_numeric($id) && ($foundEntries = $handler->find_event($options['content'], 'exact'))) + { + $id = array_shift($foundEntries); + } + if (!($cal_id = $handler->importVCal($options['content'],is_numeric($id) ? $id : -1, self::etag2value($this->http_if_match)))) { @@ -325,11 +465,85 @@ class calendar_groupdav extends groupdav_handler return true; } + /** + * Fix event series with exceptions, called by calendar_ical::importVCal(): + * a) only series master = first event got cal_id from URL + * b) exceptions need to be checked if they are already in DB or new + * c) recurrence-id of (real not virtual) exceptions need to be re-added to master + * + * @param array &$events + */ + static function fix_series(array &$events) + { + foreach($events as $n => $event) error_log(__METHOD__." $n before: ".array2string($event)); + //$master =& $events[0]; + + $bo = new calendar_boupdate(); + + // get array with orginal recurrences indexed by recurrence-id + $org_recurrences = $exceptions = array(); + foreach(self::get_series($events[0]['uid'],$bo) as $k => $event) + { + if (!$k) $master = $event; + if ($event['reference']) + { + $org_recurrences[$event['reference']] = $event; + } + } + + // assign cal_id's to already existing recurrences and evtl. re-add recur_exception to master + foreach($events as $k => &$recurrence) + { + if (!$recurrence['reference']) + { + // master + $recurrence['id'] = $master['id']; + $master =& $events[$k]; + continue; + } + + // from now on we deal with exceptions + $org_recurrence = $org_recurrences[$recurrence['reference']]; + if (isset($org_recurrence)) // already existing recurrence + { + error_log(__METHOD__.'() setting id #'.$org_recurrence['id']).' for '.$recurrence['reference'].' = '.date('Y-m-d H:i:s',$recurrence['reference']); + $recurrence['id'] = $org_recurrence['id']; + + // re-add (non-virtual) exceptions to master's recur_exception + if ($recurrence['id'] != $master['id']) + { + error_log(__METHOD__.'() re-adding recur_exception '.$recurrence['reference'].' = '.date('Y-m-d H:i:s',$recurrence['reference'])); + $exceptions[] = $recurrence['reference']; + } + // remove recurrence to be able to detect deleted exceptions + unset($org_recurrences[$recurrence['reference']]); + } + } + $master['recur_exception'] = array_merge($exceptions, $master['recur_exception']); + + // delete not longer existing recurrences + foreach($org_recurrences as $org_recurrence) + { + if ($org_recurrence['id'] != $master['id']) // non-virtual recurrence + { + error_log(__METHOD__.'() deleting #'.$org_recurrence['id']); + $bo->delete($org_recurrence['id']); // might fail because of permissions + } + else // virtual recurrence + { + error_log(__METHOD__.'() ToDO: delete virtual exception '.$org_recurrence['reference'].' = '.date('Y-m-d H:i:s',$org_recurrence['reference'])); + // todo: reset status and participants to master default + } + } + foreach($events as $n => $event) error_log(__METHOD__." $n after: ".array2string($event)); + } + /** * Handle delete request for an event * * If current user has no right to delete the event, but is an attendee, we reject the event for him. * + * @todo remove (non-virtual) exceptions, if series master gets deleted * @param array &$options * @param int $id * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') @@ -372,21 +586,41 @@ class calendar_groupdav extends groupdav_handler */ function get_etag($entry) { - $e_in = $entry; if (!is_array($entry)) { $entry = $this->read($entry); } - if (!$entry['id'] || !isset($entry['etag']) || !isset($entry['participants'])) - { - if ($this->debug > 1) error_log(__FILE__ . __METHOD__."($e_in): id=$entry[id], etag=$entry[etag], isset(participants)=".(int)isset($entry['participants']).", title=$entry[title]: id, etag or participants not set!!!"); - } $etag = $entry['id'].':'.$entry['etag']; - // add a hash over the participants and their stati - ksort($entry['participants']); // create a defined order - $etag .= ':'.md5(serialize($entry['participants'])); - //error_log(__FILE__ .__METHOD__ . "($entry[id] ($entry[etag]): $entry[title] --> etag=$etag"); - return $etag; + + // use new MAX(modification date) of egw_cal_user table (deals with virtual exceptions too) + if (isset($entry['max_user_modified'])) + { + $etag .= ':'.$entry['max_user_modified']; + } + else + { + $etag .= ':'.$this->bo->so->max_user_modified($entry['id']); + } + // include exception etags into our own etag, if exceptions are included + if ($this->client_shared_uid_exceptions && !empty($entry['uid']) && + $entry['recur_type'] != MCAL_RECUR_NONE && $entry['recur_exception']) + { + $events =& $this->bo->search(array( + 'query' => array('cal_uid' => $entry['uid']), + 'daywise' => false, + 'enum_recuring' => false, + 'date_format' => 'server', + )); + foreach($events as $k => &$recurrence) + { + if ($recurrence['reference']) // ignore series master + { + $etag .= ':'.substr($this->get_etag($recurrence),1,-1); + } + } + } + //error_log(__METHOD__ . "($entry[id] ($entry[etag]): $entry[title] --> etag=$etag"); + return '"'.$etag.'"'; } /** diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index fe7da77448..0ead233939 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -42,7 +42,7 @@ class calendar_ical extends calendar_boupdate 'A' => 'ACCEPTED', 'R' => 'DECLINED', 'T' => 'TENTATIVE', - 'D' => 'DELEGATED', + 'D' => 'DELEGATED' ); /** * @var array conversation of the participant status ical => egw @@ -75,7 +75,7 @@ class calendar_ical extends calendar_boupdate 5 => 2, // normal 4 => 3, 3 => 3, 2 => 3, 1 => 3, // high ); - + /** * @var array $priority_egw2funambol conversion of the priority egw => funambol */ @@ -210,8 +210,8 @@ class calendar_ical extends calendar_boupdate 'LOCATION' => 'location', 'DTSTART' => 'start', 'DTEND' => 'end', - 'ORGANIZER' => 'owner', 'ATTENDEE' => 'participants', + 'ORGANIZER' => 'owner', 'RRULE' => 'recur_type', 'EXDATE' => 'recur_exception', 'PRIORITY' => 'priority', @@ -220,6 +220,7 @@ class calendar_ical extends calendar_boupdate 'UID' => 'uid', 'RECURRENCE-ID' => 'reference', 'SEQUENCE' => 'etag', + 'STATUS' => 'status', ); if (!is_array($this->supportedFields)) $this->setSupportedFields(); @@ -238,25 +239,102 @@ class calendar_ical extends calendar_boupdate if (!is_array($events)) $events = array($events); - while (($event = array_pop($events))) + foreach ($events as $event) { + $mailtoOrganizer = false; + $organizerCN = false; + $recurrence = $this->date2usertime($recur_date); + if (!is_array($event) + && !($event = $this->read($event, $recurrence, false, 'server'))) + { + if ($this->read($event, $recurrence, true, 'server')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() User does not have the permission to read event ' . $event['id']. "\n", + 3,$this->logfile); + } + return -1; // Permission denied + } + else + { + $retval = false; // Entry does not exist + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Event $event not found.\n", + 3, $this->logfile); + } + } + continue; + } + if ($this->isWholeDay($event)) $event['whole_day'] = true; + if (strpos($this->productName, 'palmos')) { $utc = false; - $date_format = 'ts'; + + if (isset($event['whole_day'])) + { + if (isset($event['reference'])) + { + $event['reference'] = mktime(0, 0, 0, + date('m', $event['reference']), + date('d', $event['reference']), + date('Y', $event['reference']) + ); + } + + foreach((array)$event['recur_exception'] as $n => $date) + { + $event['recur_exception'][$n] = mktime(0, 0, 0, + date('m', $date), + date('d', $date), + date('Y', $date) + ); + } + if (isset($event['alarm']) && is_array($event['alarm'])) + { + foreach($event['alarm'] as $n => $alarm) + { + $event['alarm'][$n]['time'] = $this->date2usertime($alarm['time']); + } + } + } + else + { + $new_events = array($event); + $this->db2data($new_events, 'ts'); + $event = array_shift($new_events); + } } else { $utc = true; - $date_format = 'server'; } - if (!is_array($event) - && !($event = $this->read($event, $recur_date, false, $date_format))) + + if ($this->log) { - return false; // no permission to read $cal_id + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['id']. ',' . $recurrence . ")\n" . + array2string($event)."\n",3,$this->logfile); } - if ($recur_date) + + if ($recurrence) { + if (!isset($this->supportedFields['participants'])) + { + // We don't need status only exceptions + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($_id, $recurrence) Gratuitous pseudo exception, skipped ...\n", + 3,$this->logfile); + } + continue; // unsupported status only exception + } + // force single event foreach (array('recur_enddate','recur_interval','recur_exception','recur_data','recur_date','id','etag') as $name) { @@ -266,18 +344,13 @@ class calendar_ical extends calendar_boupdate } elseif ($event['recur_enddate']) { - $delta = $event['end'] - mktime(0, 0, 0, - date('m', $event['start']), - date('d', $event['start']), - date('Y', $event['start']) + $event['recur_enddate'] = mktime(23, 59, 59, + date('m', $event['recur_enddate']), + date('d', $event['recur_enddate']), + date('Y', $event['recur_enddate']) ); - if ($delta == 0) $delta = 24 * 60 * 60; - - $event['recur_enddate'] += $delta; } - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($event)."\n",3,$this->logfile); - if (!$serverTZ && date('e', $event['start']) != 'UTC' && ($event['recur_type'] != MCAL_RECUR_NONE || $this->useServerTZ)) @@ -327,6 +400,28 @@ class calendar_ical extends calendar_boupdate } } + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + $exceptions = array(); + + // dont use "virtual" exceptions created by participant status for GroupDAV or file export + if (!in_array($this->productManufacturer,array('file','groupdav'))) + { + $exceptions = $this->so->get_recurrence_exceptions($event); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(PSEUDO EXCEPTIONS)\n" . + array2string($exceptions)."\n",3,$this->logfile); + } + } + if (is_array($event['recur_exception'])) + { + $exceptions = array_unique(array_merge($exceptions, $event['recur_exception'])); + sort($exceptions); + } + $event['recur_exception'] = $exceptions; + } + foreach ($egwSupportedFields as $icalFieldName => $egwFieldName) { if (!isset($this->supportedFields[$egwFieldName])) continue; @@ -339,11 +434,17 @@ class calendar_ical extends calendar_boupdate foreach ((array)$event['participants'] as $uid => $status) { if (!($info = $this->resource_info($uid))) continue; - if ($uid == $event['owner'] && $status == 'A') continue; // Organizer + $mailtoParticipant = $info['email'] ? 'MAILTO:'.$info['email'] : ''; + $participantCN = '"' . ($info['cn'] ? $info['cn'] : $info['name']) . '"'; + calendar_so::split_status($status, $quantity, $role); + if ($role == 'CHAIR' && $uid != $this->user) + { + $mailtoOrganizer = $mailtoParticipant; + $organizerCN = $participantCN; + if ($status == 'U') continue; // saved ORGANIZER + } // RB: MAILTO href contains only the email-address, NO cn! - $attributes['ATTENDEE'][] = $info['email'] ? 'MAILTO:'.$info['email'] : ''; - // ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT|X-*} - calendar_so::split_status($status,$quantity,$role); + $attributes['ATTENDEE'][] = $mailtoParticipant; // RSVP={TRUE|FALSE} // resonse expected, not set in eGW => status=U $rsvp = $status == 'U' ? 'TRUE' : 'FALSE'; // PARTSTAT={NEEDS-ACTION|ACCEPTED|DECLINED|TENTATIVE|DELEGATED|COMPLETED|IN-PROGRESS} everything from delegated is NOT used by eGW atm. @@ -366,8 +467,9 @@ class calendar_ical extends calendar_boupdate $cutype = 'UNKNOWN'; break; }; + // ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT|X-*} $parameters['ATTENDEE'][] = array( - 'CN' => '"'.($info['cn'] ? $info['cn'] : $info['name']).'"', + 'CN' => $participantCN, 'ROLE' => $role, 'PARTSTAT' => $status, 'CUTYPE' => $cutype, @@ -378,34 +480,52 @@ class calendar_ical extends calendar_boupdate break; case 'CLASS': - $attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'CONFIDENTIAL'; + if ($this->productManufacturer == 'funambol') + { + $attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'PRIVATE'; + } + else + { + $attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'CONFIDENTIAL'; + } break; - case 'ORGANIZER': // according to iCalendar standard, ORGANIZER not used for events in the own calendar - if ($event['owner'] != $this->user || $this->productManufacturer != 'groupdav') + case 'ORGANIZER': + // according to iCalendar standard, ORGANIZER not used for events in the own calendar + if (!$organizerCN && + ($event['owner'] != $this->user + || $this->productManufacturer != 'groupdav')) { $mailtoOrganizer = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_email'); - $attributes['ORGANIZER'] = $mailtoOrganizer ? 'MAILTO:'.$mailtoOrganizer : ''; - $parameters['ORGANIZER']['CN'] = '"'.trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname').' '. - $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')).'"'; + $mailtoOrganizer = $mailtoOrganizer ? 'MAILTO:'.$mailtoOrganizer : ''; + $organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname') + . ' ' . $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')) . '"'; + } + if ($organizerCN) + { + $attributes['ORGANIZER'] = $mailtoOrganizer; + $parameters['ORGANIZER']['CN'] = $organizerCN; } break; case 'DTSTART': - if ($utc) + if (!isset($event['whole_day'])) { - $attributes['DTSTART'] = $event['start']; - } - else - { - $attributes['DTSTART'] = date('Ymd\THis', $event['start']); - if ($serverTZ) $parameters['DTSTART']['TZID'] = $serverTZ; + if ($utc) + { + $attributes['DTSTART'] = $event['start']; + } + else + { + $attributes['DTSTART'] = date('Ymd\THis', $event['start']); + if ($serverTZ) $parameters['DTSTART']['TZID'] = $serverTZ; + } } break; case 'DTEND': // write start + end of whole day events as dates - if ($this->isWholeDay($event)) + if (isset($event['whole_day'])) { $event['end-nextday'] = $event['end'] + 12*3600; // we need the date of the next day, as DTEND is non-inclusive (= exclusive) in rfc2445 foreach (array('start' => 'DTSTART','end-nextday' => 'DTEND') as $f => $t) @@ -507,22 +627,11 @@ class calendar_ical extends calendar_boupdate case 'EXDATE': if ($event['recur_type'] == MCAL_RECUR_NONE) break; - $days = array(); - // dont use "virtual" exceptions created by participant status for GroupDAV or file export - if (!in_array($this->productManufacturer,array('file','groupdav'))) - { - $days = $this->so->get_recurrence_exceptions($event); - } - if (is_array($event['recur_exception'])) - { - $days = array_merge($days,$event['recur_exception']); // can NOT use +, as it overwrites numeric indexes - } + $days = $event['recur_exception']; if (!empty($days)) { - $days = array_unique($days); - sort($days); // use 'DATE' instead of 'DATE-TIME' on whole day events - if ($this->isWholeDay($event)) + if (isset($event['whole_day'])) { $value_type = 'DATE'; foreach ($days as $id => $timestamp) @@ -555,7 +664,9 @@ class calendar_ical extends calendar_boupdate break; case 'PRIORITY': - if($this->productManufacturer == 'funambol') + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) { $attributes['PRIORITY'] = (int) $this->priority_egw2funambol[$event['priority']]; } @@ -576,11 +687,21 @@ class calendar_ical extends calendar_boupdate } break; + case 'STATUS': + $attributes['STATUS'] = 'CONFIRMED'; + break; + case 'CATEGORIES': - if ($event['category']) + if ($event['category'] && ($values['CATEGORIES'] = $this->get_categories($event['category']))) { - $attributes['CATEGORIES'] = ''; - $values['CATEGORIES'] = $this->get_categories($event['category']); + if (count($values['CATEGORIES']) == 1) + { + $attributes['CATEGORIES'] = array_shift($values['CATEGORIES']); + } + else + { + $attributes['CATEGORIES'] = ''; + } } break; @@ -592,7 +713,7 @@ class calendar_ical extends calendar_boupdate if ($recur_date) { // We handle a status only exception - if ($this->isWholeDay($event)) + if (isset($event['whole_day'])) { $arr = $this->date2array($recur_date); $vevent->setAttribute($icalFieldName, array( @@ -617,7 +738,7 @@ class calendar_ical extends calendar_boupdate } elseif ($event['reference']) { - if ($this->isWholeDay($event)) + if (isset($event['whole_day'])) { $arr = $this->date2array($event['reference']); $vevent->setAttribute($icalFieldName, array( @@ -647,8 +768,14 @@ class calendar_ical extends calendar_boupdate { $size = $this->clientProperties[$icalFieldName]['Size']; $noTruncate = $this->clientProperties[$icalFieldName]['NoTruncate']; - #Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " . - # ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log && $size > 0) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $icalFieldName Size: $size, NoTruncate: " . + ($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile); + } + //Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " . + // ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); } else { @@ -661,20 +788,29 @@ class calendar_ical extends calendar_boupdate { if ($noTruncate) { - Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size", - __FILE__, __LINE__, PEAR_LOG_WARNING); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $icalFieldName omitted due to maximum size $size\n",3,$this->logfile); + } + //Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_WARNING); continue; // skip field } // truncate the value to size $value = substr($value, 0, $size - 1); - Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size", - __FILE__, __LINE__, PEAR_LOG_INFO); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $icalFieldName truncated to maximum size $size\n",3,$this->logfile); + } + //Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_INFO); } if (!empty($value) || ($size >= 0 && !$noTruncate)) { $attributes[$icalFieldName] = $value; } - break; } } @@ -710,14 +846,34 @@ class calendar_ical extends calendar_boupdate $attributes['DTSTAMP'] = time(); foreach ($event['alarm'] as $alarmID => $alarmData) { + // skip over alarms that don't have the minimum required info + if (!$alarmData['offset'] && !$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) { continue; } + if ($alarmData['offset']) + { + $alarmData['time'] = $event['start'] - $alarmData['offset']; + } + + $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $event['description'])); + if ($version == '1.0') { + if ($event['title']) $description = $event['title']; + if ($description) + { + $values['DALARM']['snooze_time'] = ''; + $values['DALARM']['repeat count'] = ''; + $values['DALARM']['display text'] = $description; + $values['AALARM']['snooze_time'] = ''; + $values['AALARM']['repeat count'] = ''; + $values['AALARM']['display text'] = $description; + } if ($utc) { $attributes['DALARM'] = $alarmData['time']; @@ -737,15 +893,10 @@ class calendar_ical extends calendar_boupdate { // VCalendar 2.0 / RFC 2445 - $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $event['description'])); - - // skip over alarms that don't have the minimum required info - if (!$alarmData['offset'] && !$alarmData['time']) continue; - // RFC requires DESCRIPTION for DISPLAY if (!$event['title'] && !$description) continue; - if ($this->isWholeDay($event) && $alarmData['offset']) + if (isset($event['whole_day']) && $alarmData['offset']) { $alarmData['time'] = $event['start'] - $alarmData['offset']; $alarmData['offset'] = false; @@ -780,7 +931,7 @@ class calendar_ical extends calendar_boupdate foreach ($attributes as $key => $value) { - foreach (is_array($value)&&$parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData) + foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData) { $valueData = $GLOBALS['egw']->translation->convert($valueData,$GLOBALS['egw']->translation->charset(),'UTF-8'); $paramData = (array) $GLOBALS['egw']->translation->convert(is_array($value) ? @@ -788,29 +939,60 @@ class calendar_ical extends calendar_boupdate $GLOBALS['egw']->translation->charset(),'UTF-8'); $valuesData = (array) $GLOBALS['egw']->translation->convert($values[$key], $GLOBALS['egw']->translation->charset(),'UTF-8'); - //echo "$key:$valueID: value=$valueData, param=".print_r($paramDate,true)."\n"; - $vevent->setAttribute($key, $valueData, $paramData, true, $valuesData); - $options = array(); - if ($paramData['CN']) $valueData .= $paramData['CN']; // attendees or organizer CN can contain utf-8 content - /*if($key != 'RRULE' && preg_match('/([\000-\012\015\016\020-\037\075])/',$valueData)) { - $options['ENCODING'] = 'QUOTED-PRINTABLE'; - }*/ - if ($this->productManufacturer != 'groupdav' && preg_match('/([\177-\377])/', $valueData)) + $content = $valueData . implode(';', $valuesData); + + if (preg_match('/[^\x20-\x7F]/', $content) || + ($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))) { - $options['CHARSET'] = 'UTF-8'; + $paramData['CHARSET'] = 'UTF-8'; + switch ($this->productManufacturer) + { + case 'groupdav': + if ($this->productName == 'kde') + { + $paramData['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $paramData['CHARSET'] = ''; + if (preg_match('/([\000-\012\015\016\020-\037\075])/', $valueData)) + { + $paramData['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $paramData['ENCODING'] = ''; + } + } + break; + case 'funambol': + $paramData['ENCODING'] = 'FUNAMBOL-QP'; + } } + /* if (preg_match('/([\000-\012])/', $valueData)) { - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."() Has invalid XML data: $valueData",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() Has invalid XML data: $valueData",3,$this->logfile); + } } - $vevent->setParameter($key, $options); + */ + $vevent->setAttribute($key, $valueData, $paramData, true, $valuesData); } } $vcal->addComponent($vevent); } $retval = $vcal->exportvCalendar(); - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($retval)."\n",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() '$this->productManufacturer','$this->productName'\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "()\n".array2string($retval)."\n",3,$this->logfile); + } return $retval; } @@ -828,25 +1010,168 @@ class calendar_ical extends calendar_boupdate */ function importVCal($_vcalData, $cal_id=-1, $etag=null, $merge=false, $recur_date=0) { - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($_vcalData)."\n",3,$this->logfile); + if (!is_array($this->supportedFields)) $this->setSupportedFields(); - if (!($events = $this->icaltoegw($_vcalData,$cal_id,$etag,$recur_date))) + if (!($events = $this->icaltoegw($_vcalData))) { return false; } - if (!is_array($this->supportedFields)) $this->setSupportedFields(); + if ($cal_id > 0) + { + if (count($events) == 1) + { + $events[0]['id'] = $cal_id; + if (!is_null($etag)) $events[0]['etag'] = (int) $etag; + if ($recur_date) $events[0]['reference'] = $recur_date; + } + elseif (($foundEvent = $this->find_event(array('id' => $cal_id), 'exact')) && + ($eventId = array_shift($foundEvent)) && + ($egwEvent = $this->read($eventId))) + { + foreach ($events as $k => $event) + { + if (!isset($event['uid'])) $events[$k]['uid'] = $egwEvent['uid']; + } + } + } + // check if we are importing an event series with exceptions in CalDAV + // only first event / series master get's cal_id from URL + // other events are exceptions and need to be checked if they are new + // and for real (not status only) exceptions their recurrence-id need + // to be included as recur_exception to the master + if ($this->productManufacturer == 'groupdav' && $cal_id > 0 && + count($events) > 1 && !$events[1]['id'] && + $events[0]['recur_type'] != MCAL_RECUR_NONE) + { + calendar_groupdav::fix_series($events); + } foreach ($events as $event) { + if ($this->isWholeDay($event, true)) $event['whole_day'] = true; + if (is_array($event['category'])) + { + $event['category'] = $this->find_or_add_categories($event['category'], + isset($event['id']) ? $event['id'] : -1); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } $updated_id = false; $event_info = $this->get_event_info($event); - // common adjustments for new events - if (!is_array($event_info['stored_event'])) + // common adjustments for existing events + if (is_array($event_info['stored_event'])) { + if (empty($event['uid'])) + { + $event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered + } + elseif (empty($event['id'])) + { + $event['id'] = $event_info['stored_event']['id']; // CalDAV does only provide UIDs + } + if ($merge) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "()[MERGE]\n",3,$this->logfile); + } + + // overwrite with server data for merge + foreach ($event_info['stored_event'] as $key => $value) + { + switch ($key) + { + case 'participants_types': + continue; + + case 'participants': + foreach ($event_info['stored_event']['participants'] as $uid => $status) + { + // Is a participant and no longer present in the event? + if (!isset($event['participants'][$uid])) + { + // Add it back in + $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; + } + } + break; + + default: + if (!empty($value)) $event[$key] = $value; + } + } + } + else + { + // no merge + if (!isset($this->supportedFields['participants']) || !count($event['participants'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() No participants\n",3,$this->logfile); + } + + // If this is an updated meeting, and the client doesn't support + // participants OR the event no longer contains participants, add them back + unset($event['participants']); + unset($event['participant_types']); + } + else + { + // if the client does not return a status, we restore the original one + foreach ($event['participants'] as $uid => $status) + { + // Is it a resource and no longer present in the event? + if ($status[0] == 'X') + { + if (isset($event_info['stored_event']['participants'][$uid])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Restore status for $uid\n",3,$this->logfile); + } + $event['participants'][$uid] = $event_info['stored_event']['participants'][$uid]; + } + else + { + $event['participants'][$uid] = calendar_so::combine_status('U'); + } + } + } + } + + foreach ($event_info['stored_event']['participants'] as $uid => $status) + { + // Is it a resource and no longer present in the event? + if ($uid[0] == 'r' && !isset($event['participants'][$uid])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Restore resource $uid to status $status\n",3,$this->logfile); + } + // Add it back in + $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; + } + } + + // avoid that iCal changes the organizer, which is not allowed + $event['owner'] = $event_info['stored_event']['owner']; + } + } + else // common adjustments for new events + { + unset($event['id']); // set non blocking all day depending on the user setting - if ($this->isWholeDay($event) && $this->nonBlockingAllday) + if (isset($event['whole_day']) && $this->nonBlockingAllday) { $event['non_blocking'] = 1; } @@ -859,69 +1184,29 @@ class calendar_ical extends calendar_boupdate $event['owner'] = $this->user; } - // add ourself to new events as participant - if (!isset($this->supportedFields['participants']) - ||!isset($event['participants'][$this->user])) - { - $event['participants'][$this->user] = 'A'; - } - } - - // common adjustments for existing events - if (is_array($event_info['stored_event'])) - { - if ($merge) + if (!is_array($event['participants']) || !count($event['participants'])) { - // overwrite with server data for merge - foreach ($event_info['stored_event'] as $key => $value) - { - switch ($key) - { - case 'participants_types': - continue; - - case 'participants': - foreach ($event_info['stored_event']['participants'] as $uid => $status) - { - // Is a participant and no longer present in the event? - if (!isset($event['participants'][$uid])) - { - // Add it back in - $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; - } - } - break; - - default: - if (!empty($value)) - { - $event[$key] = $value; - } - } - } + $status = $event['owner'] == $this->user ? 'A' : 'U'; + $status = calendar_so::combine_status($status, 1, 'CHAIR'); + $event['participants'] = array($event['owner'] => $status); } else { - // no merge - if (!isset($this->supportedFields['participants']) || !count($event['participants'])) - { - // If this is an updated meeting, and the client doesn't support - // participants OR the event no longer contains participants, add them back - $event['participants'] = $event_info['stored_event']['participants']; - $event['participant_types'] = $event_info['stored_event']['participant_types']; - } - - foreach ($event_info['stored_event']['participants'] as $uid => $status) + foreach ($event['participants'] as $uid => $status) { // Is it a resource and no longer present in the event? - if ( $uid[0] == 'r' && !isset($event['participants'][$uid]) ) + if ($status[0] == 'X') { - // Add it back in - $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; + if ($uid == $event['owner']) + { + $event['participants']['uid'] = calendar_so::combine_status('A', 1, 'CHAIR'); + } + else + { + $event['participants']['uid'] = calendar_so::combine_status('U'); + } } } - // avoid that iCal changes the organizer, which is not allowed - $event['owner'] = $event_info['stored_event']['owner']; } } @@ -967,7 +1252,7 @@ class calendar_ical extends calendar_boupdate } break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': // nothing to do here break; } @@ -989,27 +1274,45 @@ class calendar_ical extends calendar_boupdate switch ($event_info['type']) { case 'SINGLE': - Horde::logMessage('importVCAL event SINGLE',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SINGLE\n",3,$this->logfile); + } // update the event if ($event_info['acl_edit']) { - $event_to_store = $event; // prevent $event from being changed by the update method + // Force SINGLE + $event['reference'] = 0; + $event_to_store = array($event); // prevent $event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); $updated_id = $this->update($event_to_store, true); unset($event_to_store); } break; case 'SERIES-MASTER': - Horde::logMessage('importVCAL event SERIES-MASTER',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-MASTER\n",3,$this->logfile); + } - // remove all known "status only" exceptions and update the event + // remove all known pseudo exceptions and update the event if ($event_info['acl_edit']) { - $days = $this->so->get_recurrence_exceptions($event); + $days = $this->so->get_recurrence_exceptions($event_info['stored_event']); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(PSEUDO EXCEPTIONS):\n" . + array2string($days)."\n",3,$this->logfile); + } if (is_array($days)) { $recur_exceptions = array(); + foreach ($event['recur_exception'] as $recur_exception) { if (!in_array($recur_exception, $days)) @@ -1020,7 +1323,9 @@ class calendar_ical extends calendar_boupdate $event['recur_exception'] = $recur_exceptions; } - $event_to_store = $event; // prevent $event from being changed by the update method + $event_to_store = array($event); // prevent $event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); $updated_id = $this->update($event_to_store, true); unset($event_to_store); } @@ -1028,7 +1333,11 @@ class calendar_ical extends calendar_boupdate case 'SERIES-EXCEPTION': case 'SERIES-EXCEPTION-PROPAGATE': - Horde::logMessage('importVCAL event SERIES-EXCEPTION',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-EXCEPTION\n",3,$this->logfile); + } // update event if ($event_info['acl_edit']) @@ -1043,23 +1352,36 @@ class calendar_ical extends calendar_boupdate { // We create a new exception unset($event['id']); - $event_info['master_event']['recur_exception'] = array_unique(array_merge($event_info['master_event']['recur_exception'], array($event['reference']))); - $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method - $this->update($event_to_store, true); - unset($event_to_store); - $event['reference'] = $event_info['master_event']['id']; + unset($event_info['stored_event']); + $event['recur_type'] = MCAL_RECUR_NONE; + $event_info['master_event']['recur_exception'] = + array_unique(array_merge($event_info['master_event']['recur_exception'], + array($event['reference']))); $event['category'] = $event_info['master_event']['category']; $event['owner'] = $event_info['master_event']['owner']; + $event_to_store = array($event_info['master_event']); // prevent the master_event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $this->update($event_to_store, true); + unset($event_to_store); } - $event_to_store = $event; // prevent $event from being changed by update method - $updated_id = $this->update($event_to_store, true, true, false, false); + $event_to_store = array($event); // prevent $event from being changed by update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $updated_id = $this->update($event_to_store, true); unset($event_to_store); } break; - case 'SERIES-EXCEPTION-STATUS': - Horde::logMessage('importVCAL event SERIES-EXCEPTION-STATUS',__FILE__, __LINE__, PEAR_LOG_DEBUG); + case 'SERIES-PSEUDO-EXCEPTION': + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-PSEUDO-EXCEPTION\n",3,$this->logfile); + } + //Horde::logMessage('importVCAL event SERIES-PSEUDO-EXCEPTION', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); if ($event_info['acl_edit']) { @@ -1075,18 +1397,19 @@ class calendar_ical extends calendar_boupdate $event_info['master_event']['recur_exception'] = $recur_exceptions; // save the series master with the adjusted exceptions - $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method + $event_to_store = array($event_info['master_event']); // prevent the master_event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); $updated_id = $this->update($event_to_store, true, true, false, false); unset($event_to_store); } - break; } // read stored event into info array for fresh stored (new) events if (!is_array($event_info['stored_event']) && $updated_id > 0) { - $event_info['stored_event'] = $this->read($updated_id); + $event_info['stored_event'] = $this->read($updated_id, 0, false, 'server'); } // update status depending on the given event type @@ -1112,19 +1435,20 @@ class calendar_ical extends calendar_boupdate } break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': if (is_array($event_info['master_event'])) // status update requires a stored master event { + $recurrence = $this->date2usertime($event['reference']); if ($event_info['acl_edit']) { // update all participants if we have the right to do that - $this->update_status($event, $event_info['master_event'], $event['reference']); + $this->update_status($event, $event_info['stored_event'], $recurrence); } elseif (isset($event['participants'][$this->user]) || isset($event_info['master_event']['participants'][$this->user])) { // update the users status only $this->set_status($event_info['master_event']['id'], $this->user, - ($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $event['reference'], true); + ($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $recurrence, true); } } break; @@ -1139,7 +1463,7 @@ class calendar_ical extends calendar_boupdate $return_id = is_array($event_info['stored_event']) ? $event_info['stored_event']['id'] : false; break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': $return_id = is_array($event_info['master_event']) ? $event_info['master_event']['id'] . ':' . $event['reference'] : false; break; @@ -1152,7 +1476,7 @@ class calendar_ical extends calendar_boupdate else { // we did not have sufficient rights to propagate the status only exception to a real one - // we have to keep the SERIES-EXCEPTION-STATUS id and keep the event untouched + // we have to keep the SERIES-PSEUDO-EXCEPTION id and keep the event untouched $return_id = $event_info['master_event']['id'] . ':' . $event['reference']; } break; @@ -1160,11 +1484,11 @@ class calendar_ical extends calendar_boupdate if ($this->log) { - $egw_event = $this->read($event['id']); - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($egw_event)."\n",3,$this->logfile); + $event_info['stored_event'] = $this->read($event_info['stored_event']['id']); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[$updated_id]\n" . + array2string($event_info['stored_event'])."\n",3,$this->logfile); } } - return $return_id; } @@ -1239,13 +1563,13 @@ class calendar_ical extends calendar_boupdate function setSupportedFields($_productManufacturer='', $_productName='') { - $state = &$_SESSION['SyncML.state']; + $state =& $_SESSION['SyncML.state']; if (isset($state)) { $deviceInfo = $state->getClientDeviceInfo(); } - // store product manufacturer and name, to be able to use it elsewhere + // store product manufacturer and name for further usage if ($_productManufacturer) { $this->productManufacturer = strtolower($_productManufacturer); @@ -1272,6 +1596,11 @@ class calendar_ical extends calendar_boupdate if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner'])) { $owner = $GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner']; + if ($owner == 0) + { + $owner = $GLOBALS['egw_info']['user']['account_primary_group']; + } + if (0 < (int)$owner && $this->check_perms(EGW_ACL_EDIT,0,$owner)) { $this->calendarOwner = $owner; @@ -1289,8 +1618,19 @@ class calendar_ical extends calendar_boupdate } } - Horde::logMessage('setSupportedFields(' . $this->productManufacturer - . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $this->productManufacturer . + ', '. $this->productName .', ' . + ($this->useServerTZ ? 'TRUE' : 'FALSE') . + ")\n" , 3, $this->logfile); + } + + //Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' + // . $this->productName .', ' . + // ($this->useServerTZ ? 'TRUE' : 'FALSE') .')', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); $defaultFields['minimal'] = array( 'public' => 'public', @@ -1309,6 +1649,7 @@ class calendar_ical extends calendar_boupdate $defaultFields['basic'] = $defaultFields['minimal'] + array( 'recur_exception' => 'recur_exception', 'priority' => 'priority', + 'status' => 'status', ); $defaultFields['nexthaus'] = $defaultFields['basic'] + array( @@ -1331,6 +1672,13 @@ class calendar_ical extends calendar_boupdate 'etag' => 'etag', ); + $defaultFields['funambol'] = $defaultFields['basic'] + array( + 'participants' => 'participants', + 'owner' => 'owner', + 'category' => 'category', + 'non_blocking' => 'non_blocking', + ); + $defaultFields['evolution'] = $defaultFields['basic'] + array( 'participants' => 'participants', 'owner' => 'owner', @@ -1458,7 +1806,7 @@ class calendar_ical extends calendar_boupdate break; case 'funambol': - $this->supportedFields = $defaultFields['synthesis']; + $this->supportedFields = $defaultFields['funambol']; break; // the fallback for SyncML @@ -1469,12 +1817,30 @@ class calendar_ical extends calendar_boupdate } } - function icaltoegw($_vcalData, $cal_id=-1, $etag=null, $recur_date=0) + function icaltoegw($_vcalData) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_vcalData)."\n",3,$this->logfile); + } + $events = array(); $vcal = new Horde_iCalendar; - if (!$vcal->parsevCalendar($_vcalData)) return false; + if (!$vcal->parsevCalendar($_vcalData)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): No vCalendar Container found!\n",3,$this->logfile); + } + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + return false; + } $version = $vcal->getAttribute('VERSION'); if (!is_array($this->supportedFields)) $this->setSupportedFields(); @@ -1482,8 +1848,9 @@ class calendar_ical extends calendar_boupdate { if (is_a($component, 'Horde_iCalendar_vevent')) { - if ($event = $this->vevent2egw($component, $version, $this->supportedFields, $cal_id)) + if ($event = $this->vevent2egw($component, $version, $this->supportedFields)) { + if ($this->isWholeDay($event)) $event['whole_day'] = true; //common adjustments if ($this->productManufacturer == '' && $this->productName == '' && !empty($event['recur_enddate'])) @@ -1491,6 +1858,7 @@ class calendar_ical extends calendar_boupdate // syncevolution needs an adjusted recur_enddate $event['recur_enddate'] = (int)$event['recur_enddate'] + 86400; } + if ($event['recur_type'] != MCAL_RECUR_NONE) { // No reference or RECURRENCE-ID for the series master @@ -1508,28 +1876,78 @@ class calendar_ical extends calendar_boupdate } $event['alarm'] = $alarms; + if (strpos($this->productName, 'palmos')) + { + if (isset($event['whole_day'])) + { + $event['start'] = mktime(0, 0, 0, + date('m', $event['start']), + date('d', $event['start']), + date('Y', $event['start']) + ); + $event['end'] = mktime(23, 59, 59, + date('m', $event['end']), + date('d', $event['end']), + date('Y', $event['end']) + ); + if (isset($event['reference'])) + { + $event['reference'] = mktime(0, 0, 0, + date('m', $event['reference']), + date('d', $event['reference']), + date('Y', $event['reference']) + ); + } + foreach($event['recur_exception'] as $n => $date) + { + $event['recur_exception'][$n] = mktime(0, 0, 0, + date('m', $date), + date('d', $date), + date('Y', $date) + ); + } + } + else + { + foreach(array('start','end','recur_enddate','reference') as $ts) + { + // we convert here from user-time to timestamps in server-time! + if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2ts($event[$ts],true) : 0; + } + // same with the recur exceptions + if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + { + foreach($event['recur_exception'] as $n => $date) + { + $event['recur_exception'][$n] = $this->date2ts($date,true); + } + } + } + // same with the alarms + if (isset($event['alarm']) && is_array($event['alarm'])) + { + foreach($event['alarm'] as &$alarm) + { + $alarm['time'] = $this->date2ts($alarm['time'],true); + } + } + } + if (!empty($event['recur_enddate'])) + { + // reset recure_enddate to 00:00:00 + $event['recur_enddate'] = mktime(0, 0, 0, + date('m', $event['recur_enddate']), + date('d', $event['recur_enddate']), + date('Y', $event['recur_enddate']) + ); + } + $events[] = $event; } } } - // decide what to return - if (count($events) == 1) - { - $event = array_shift($events); - if ($cal_id > 0) $event['id'] = $cal_id; - if (!is_null($etag)) $event['etag'] = $etag; - if ($recur_date) $event['reference'] = $recur_date; - - return array($event); - } - elseif (count($events) == 0 || $cal_id > 0 || !is_null($etag) || $recur_date) - { - // no events to return - // or not allowed N:1 relation with params just meant for a single event - return false; - } - else return $events; + return $events; } /** @@ -1538,10 +1956,10 @@ class calendar_ical extends calendar_boupdate * @param array $component VEVENT * @param string $version vCal version (1.0/2.0) * @param array $supportedFields supported fields of the device - * @param int $cal_id id of existing event in the content (only used to merge categories) + * * @return array|boolean event on success, false on failure */ - function vevent2egw(&$component, $version, $supportedFields, $cal_id=-1) + function vevent2egw(&$component, $version, $supportedFields) { if (!is_a($component, 'Horde_iCalendar_vevent')) return false; @@ -1554,7 +1972,6 @@ class calendar_ical extends calendar_boupdate } $isDate = false; - $hasOrganizer = false; $event = array(); $alarms = array(); $vcardData = array( @@ -1579,7 +1996,7 @@ class calendar_ical extends calendar_boupdate $dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']); if (date('H:i:s',$dtend_ts) == '00:00:00') { - $dtend_ts -= 60; + $dtend_ts -= 1; } $vcardData['end'] = $dtend_ts; } @@ -1602,7 +2019,7 @@ class calendar_ical extends calendar_boupdate $vcardData['public'] = (int)(strtolower($attributes['value']) == 'public'); break; case 'DESCRIPTION': - $vcardData['description'] = $attributes['value']; + $vcardData['description'] = str_replace("\r\n", "\n", $attributes['value']); if (preg_match('/\s*\[UID:(.+)?\]/Usm', $attributes['value'], $matches)) { if (!isset($vCardData['uid']) @@ -1617,10 +2034,11 @@ class calendar_ical extends calendar_boupdate $vcardData['reference'] = $attributes['value']; break; case 'LOCATION': - $vcardData['location'] = $attributes['value']; + $vcardData['location'] = str_replace("\r\n", "\n", $attributes['value']); break; case 'RRULE': $recurence = $attributes['value']; + $vcardData['recur_interval'] = 1; $type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0]; // vCard 2.0 values for all types if (preg_match('/UNTIL=([0-9TZ]+)/',$recurence,$matches)) @@ -1633,8 +2051,7 @@ class calendar_ical extends calendar_boupdate } if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches)) { - // 1 is invalid,, egw uses 0 for interval - $vcardData['recur_interval'] = (int) $matches[1] != 0 ? (int) $matches[1] : 0; + $vcardData['recur_interval'] = (int) $matches[1] ? (int) $matches[1] : 1; } $vcardData['recur_data'] = 0; switch($type) @@ -1642,26 +2059,27 @@ class calendar_ical extends calendar_boupdate case 'W': case 'WEEKLY': $days = array(); - if (preg_match('/W(\d+) ([^ ]*)( ([^ ]*))?$/',$recurence, $recurenceMatches)) // 1.0 + if (preg_match('/W(\d+) *((?i: [AEFHMORSTUW]{2})+)?( +([^ ]*))$/',$recurence, $recurenceMatches)) // 1.0 { $vcardData['recur_interval'] = $recurenceMatches[1]; - if (empty($recurenceMatches[4])) + if (empty($recurenceMatches[2])) { - if ($recurenceMatches[2] != '#0') - { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); - } $days[0] = strtoupper(substr(date('D', $vcardData['start']),0,2)); } else { $days = explode(' ',trim($recurenceMatches[2])); - - if ($recurenceMatches[4] != '#0') - { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); - } } + + if (preg_match('/#(\d+)/',$recurenceMatches[4],$repeatMatches)) + { + if ($repeatMatches[1]) $vcardData['recur_count'] = $repeatMatches[1]; + } + else + { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); + } + $recur_days = $this->recur_days_1_0; } elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0 @@ -1699,7 +2117,7 @@ class calendar_ical extends calendar_boupdate break; case 'D': // 1.0 - if (preg_match('/D(\d+) #(.\d)/', $recurence, $recurenceMatches)) + if (preg_match('/D(\d+) #(\d+)/', $recurence, $recurenceMatches)) { $vcardData['recur_interval'] = $recurenceMatches[1]; if ($recurenceMatches[2] > 0 && $vcardData['end']) @@ -1709,7 +2127,7 @@ class calendar_ical extends calendar_boupdate date('i', $vcardData['end']), date('s', $vcardData['end']), date('m', $vcardData['end']), - date('d', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']), + date('d', $vcardData['end']) + ($vcardData['recur_interval']*($recurenceMatches[2]-1)), date('Y', $vcardData['end']) ); } @@ -1717,10 +2135,7 @@ class calendar_ical extends calendar_boupdate elseif (preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches)) { $vcardData['recur_interval'] = $recurenceMatches[1]; - if ($recurenceMatches[2] != '#0') - { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); - } + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2])); } else break; @@ -1741,7 +2156,7 @@ class calendar_ical extends calendar_boupdate break; case 'M': - if (preg_match('/MD(\d+) #(.\d)/', $recurence, $recurenceMatches)) + if (preg_match('/MD(\d+)(?: [^ ])? #(\d+)/', $recurence, $recurenceMatches)) { $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY; $vcardData['recur_interval'] = $recurenceMatches[1]; @@ -1751,7 +2166,7 @@ class calendar_ical extends calendar_boupdate date('H', $vcardData['end']), date('i', $vcardData['end']), date('s', $vcardData['end']), - date('m', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']), + date('m', $vcardData['end']) + ($vcardData['recur_interval']*($recurenceMatches[2]-1)), date('d', $vcardData['end']), date('Y', $vcardData['end']) ); @@ -1764,21 +2179,28 @@ class calendar_ical extends calendar_boupdate { $vcardData['recur_interval'] = $recurenceMatches[1]; } - if ($recurenceMatches[2] != '#0') - { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); - } + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2])); } elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches)) { $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_WDAY; - if ($recurenceMatches[1] > 1) + $vcardData['recur_interval'] = $recurenceMatches[1]; + if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches)) { - $vcardData['recur_interval'] = $recurenceMatches[1]; + if ($recurenceMatches[1]) + { + $vcardData['recur_enddate'] = mktime( + date('H', $vcardData['end']), + date('i', $vcardData['end']), + date('s', $vcardData['end']), + date('m', $vcardData['start']) + ($vcardData['recur_interval']*($recurenceMatches[1]-1)), + date('d', $vcardData['start']), + date('Y', $vcardData['start'])); + } } - if ($recurenceMatches[4] != '#0') + else { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[4])); } } break; @@ -1819,7 +2241,7 @@ class calendar_ical extends calendar_boupdate $vcardData['recur_interval'] = $recurenceMatches[1]; if ($recurenceMatches[2] != '#0') { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2])); } } else break; @@ -1841,6 +2263,7 @@ class calendar_ical extends calendar_boupdate } break; case 'EXDATE': + if (!$attributes['value']) break; if ((isset($attributes['params']['VALUE']) && $attributes['params']['VALUE'] == 'DATE') || (!isset($attributes['params']['VALUE']) && $isDate)) @@ -1867,7 +2290,7 @@ class calendar_ical extends calendar_boupdate } break; case 'SUMMARY': - $vcardData['title'] = $attributes['value']; + $vcardData['title'] = str_replace("\r\n", "\n", $attributes['value']); break; case 'UID': if (strlen($attributes['value']) >= $minimum_uid_length) @@ -1886,7 +2309,9 @@ class calendar_ical extends calendar_boupdate } break; case 'PRIORITY': - if($this->productManufacturer == 'funambol') + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) { $vcardData['priority'] = (int) $this->priority_funambol2egw[$attributes['value']]; } @@ -1898,22 +2323,15 @@ class calendar_ical extends calendar_boupdate case 'CATEGORIES': if ($attributes['value']) { - if($version == '1.0') - { - $vcardData['category'] = $this->find_or_add_categories(explode(';',$attributes['value']), $cal_id); - } - else - { - $vcardData['category'] = $this->find_or_add_categories(explode(',',$attributes['value']), $cal_id); - } + $vcardData['category'] = explode(',', $attributes['value']); } else { $vcardData['category'] = array(); } break; - case 'ORGANIZER': // will be written direct to the event case 'ATTENDEE': + case 'ORGANIZER': // will be written direct to the event if (isset($attributes['params']['PARTSTAT'])) { $attributes['params']['STATUS'] = $attributes['params']['PARTSTAT']; @@ -1921,10 +2339,11 @@ class calendar_ical extends calendar_boupdate if (isset($attributes['params']['STATUS'])) { $status = $this->status_ical2egw[strtoupper($attributes['params']['STATUS'])]; + if (empty($status)) $status = 'X'; } else { - $status = 0; + $status = 'X'; // client did not return the status } $cn = ''; if (preg_match('/MAILTO:([@.a-z0-9_-]+)|MAILTO:"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i', @@ -1979,7 +2398,7 @@ class calendar_ical extends calendar_boupdate { //Horde::logMessage("vevent2egw: group participant $uid", // __FILE__, __LINE__, PEAR_LOG_DEBUG); - if ($status != 'U') + if ($status != 'X' && $status != 'U') { // User tries to reply to the group invitiation $members = $GLOBALS['egw']->accounts->members($uid, true); @@ -1987,7 +2406,8 @@ class calendar_ical extends calendar_boupdate { //Horde::logMessage("vevent2egw: set status to " . $status, // __FILE__, __LINE__, PEAR_LOG_DEBUG); - $event['participants'][$this->user] = $status; + $event['participants'][$this->user] = + calendar_so::combine_status($status); } $status = 'U'; // keep the group } @@ -2022,18 +2442,26 @@ class calendar_ical extends calendar_boupdate switch($attributes['name']) { case 'ATTENDEE': - if (!$status) - { - $status = ($uid == $event['owner'] ? 'A' : 'U'); - } - if (!isset($attributes['params']['ROLE']) || $attributes['params']['ROLE'] != 'ORGANIZER') + if (!isset($event['participants'][$uid]) || + $event['participants'][$uid][0] != 'A') { + // for multiple entries the ACCEPT wins // add quantity and role - $event['participants'][$uid] = calendar_so::combine_status($status,$attributes['params']['X-EGROUPWARE-QUANTITY'],$attributes['params']['ROLE']); - break; + $event['participants'][$uid] = + calendar_so::combine_status($status, + $attributes['params']['X-EGROUPWARE-QUANTITY'], + $attributes['params']['ROLE']); } + break; + case 'ORGANIZER': - $hasOrganizer = true; + if (isset($event['participants'][$uid])) + { + $status = $event['participants'][$uid]; + calendar_so::split_status($status, $quantity, $role); + $event['participants'][$uid] = + calendar_so::combine_status($status, $quantity, 'CHAIR'); + } if (is_numeric($uid) && ($uid == $this->calendarOwner || !$this->calendarOwner)) { // we can store the ORGANIZER as event owner @@ -2041,11 +2469,14 @@ class calendar_ical extends calendar_boupdate } else { + // we must insert a CHAIR participant to keep the ORGANIZER $event['owner'] = $this->user; - } - if (!isset($event['participants'][$uid])) - { - $event['participants'][$uid] = 'A'; + if (!isset($event['participants'][$uid])) + { + // save the ORGANIZER as event CHAIR + $event['participants'][$uid] = + calendar_so::combine_status('U', 1, 'CHAIR'); + } } } break; @@ -2054,16 +2485,9 @@ class calendar_ical extends calendar_boupdate // fall through case 'LAST-MODIFIED': // will be written direct to the event $event['modified'] = $attributes['value']; - break; } } - if (!$hasOrganizer && !isset($event['participants'][$this->user])) - { - // according to iCalendar standard, ORGANIZER not used for events in the own calendar - $event['participants'][$this->user] = 'A'; - } - // check if the entry is a birthday // this field is only set from NOKIA clients $agendaEntryType = $component->getAttribute('X-EPOCAGENDAENTRYTYPE'); @@ -2083,16 +2507,6 @@ class calendar_ical extends calendar_boupdate } } - if (!empty($vcardData['recur_enddate'])) - { - // reset recure_enddate to 00:00:00 on the last day - $vcardData['recur_enddate'] = mktime(0, 0, 0, - date('m',$vcardData['recur_enddate']), - date('d',$vcardData['recur_enddate']), - date('Y',$vcardData['recur_enddate']) - ); - } - $event['priority'] = 2; // default $event['alarm'] = $alarms; @@ -2133,27 +2547,38 @@ class calendar_ical extends calendar_boupdate } } if ($this->calendarOwner) $event['owner'] = $this->calendarOwner; + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } //Horde::logMessage("vevent2egw:\n" . print_r($event, true), // __FILE__, __LINE__, PEAR_LOG_DEBUG); - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($event)."\n",3,$this->logfile); - } return $event; } function search($_vcalData, $contentID=null, $relax=false) { - if (($events = $this->icaltoegw($_vcalData,!is_null($contentID) ? $contentID : -1))) + if (($events = $this->icaltoegw($_vcalData))) { // this function only supports searching a single event if (count($events) == 1) { + $filter = $relax ? 'relax' : 'check'; $event = array_shift($events); - return $this->find_event($event, $relax); + if ($this->isWholeDay($event, true)) $event['whole_day'] = true; + $event['category'] = $this->find_or_add_categories($event['category'], $eventId); + if ($contentID) $event['id'] = $contentID; + return $this->find_event($event, $filter); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."() found:\n" . + array2string($events)."\n",3,$this->logfile); } } - return false; + return array(); } /** @@ -2269,124 +2694,6 @@ class calendar_ical extends calendar_boupdate } } - /** - * classifies an incoming event from the eGW point-of-view - * - * exceptions: unlike other calendar apps eGW does not create an event exception - * if just the participant state changes - therefore we have to distinguish between - * real exceptions and status only exceptions - * - * @param array $event the event to check - * - * @return array - * type => - * SINGLE a single event - * SERIES-MASTER the series master - * SERIES-EXCEPTION event is a real exception - * SERIES-EXCEPTION-STATUS event is a status only exception - * SERIES-EXCEPTION-PROPAGATE event was a status only exception in the past and is now a real exception - * stored_event => if event already exists in the database array with event data or false - * master_event => for event type SERIES-EXCEPTION, SERIES-EXCEPTION-STATUS or SERIES-EXCEPTION-PROPAGATE - * the corresponding series master event array - * NOTE: this param is false if event is of type SERIES-MASTER - */ - private function get_event_info($event) - { - $type = 'SINGLE'; // default - $return_master = false; //default - - if ($event['recur_type'] != MCAL_RECUR_NONE) - { - $type = 'SERIES-MASTER'; - } - else - { - // SINGLE, SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS - if (empty($event['uid']) && $event['id'] > 0 && ($stored_event = $this->read($event['id']))) - { - $event['uid'] = $stored_event['uid']; // restore the UID if it was not delivered - } - - if (isset($event['uid']) - && $event['reference'] - && ($master_event = $this->read($event['uid'])) - && isset($master_event['recur_type']) - && $master_event['recur_type'] != MCAL_RECUR_NONE) - { - // SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS - $return_master = true; // we have a valid master and can return it - - if (isset($event['id']) && $master_event['id'] != $event['id']) - { - $type = 'SERIES-EXCEPTION'; // this is an existing exception - } - else - { - $type = 'SERIES-EXCEPTION-STATUS'; // default if we cannot find a proof for a fundamental change - // the recurrence_event is the master event with start and end adjusted to the recurrence - $recurrence_event = $master_event; - $recurrence_event['start'] = $event['reference']; - $recurrence_event['end'] = $event['reference'] + ($master_event['end'] - $master_event['start']); - // check for changed data - foreach (array('start','end','uid','title','location', - 'priority','public','special','non_blocking') as $key) - { - if (!empty($event[$key]) && $recurrence_event[$key] != $event[$key]) - { - if (isset($event['id'])) - { - $type = 'SERIES-EXCEPTION-PROPAGATE'; - } - else - { - $type = 'SERIES-EXCEPTION'; // this is a new exception - } - break; - } - } - // the event id here is always the id of the master event - // unset it to prevent confusion of stored event and master event - unset($event['id']); - } - } - else - { - // SINGLE - $type = 'SINGLE'; - } - } - - // read existing event - if (isset($event['id'])) - { - $stored_event = $this->read($event['id']); - } - - // check ACL - if ($return_master) - { - $acl_edit = $this->check_perms(EGW_ACL_EDIT, $master_event['id']); - } - else - { - if (is_array($stored_event)) - { - $acl_edit = $this->check_perms(EGW_ACL_EDIT, $stored_event['id']); - } - else - { - $acl_edit = true; // new event - } - } - - return array( - 'type' => $type, - 'acl_edit' => $acl_edit, - 'stored_event' => is_array($stored_event) ? $stored_event : false, - 'master_event' => $return_master ? $master_event : false, - ); - } - /** * generate and insert a VTIMEZONE entry to a vcalendar * diff --git a/calendar/inc/class.calendar_sif.inc.php b/calendar/inc/class.calendar_sif.inc.php index 77e4fa664f..855101c4a8 100644 --- a/calendar/inc/class.calendar_sif.inc.php +++ b/calendar/inc/class.calendar_sif.inc.php @@ -21,50 +21,106 @@ class calendar_sif extends calendar_boupdate var $sifMapping = array( 'Start' => 'start', 'End' => 'end', - 'AllDayEvent' => 'alldayevent', + 'AllDayEvent' => 'alldayevent', 'Attendees' => '', 'BillingInformation' => '', 'Body' => 'description', - 'BusyStatus' => '', - 'Categories' => 'category', + 'BusyStatus' => '', + 'Categories' => 'category', 'Companies' => '', - 'Importance' => 'priority', - 'IsRecurring' => 'isrecurring', + 'Importance' => 'priority', + 'IsRecurring' => 'isrecurring', 'Location' => 'location', - 'MeetingStatus' => '', + 'MeetingStatus' => '', 'Mileage' => '', 'ReminderMinutesBeforeStart' => 'reminderstart', - 'ReminderSet' => 'reminderset', - 'ReminderSoundFile' => '', - 'ReminderOptions' => '', - 'ReminderInterval' => '', + 'ReminderSet' => 'reminderset', + 'ReminderSoundFile' => '', + 'ReminderOptions' => '', + 'ReminderInterval' => '', 'ReminderRepeatCount' => '', - 'Exceptions' => '', + 'Exceptions' => '', 'ReplyTime' => '', - 'Sensitivity' => 'public', + 'Sensitivity' => 'public', 'Subject' => 'title', - 'RecurrenceType' => 'recur_type', + 'RecurrenceType' => 'recur_type', 'Interval' => 'recur_interval', - 'MonthOfYear' => '', - 'DayOfMonth' => '', - 'DayOfWeekMask' => 'recur_weekmask', + 'MonthOfYear' => '', + 'DayOfMonth' => '', + 'DayOfWeekMask' => 'recur_weekmask', 'Instance' => '', - 'PatternStartDate' => '', + 'PatternStartDate' => '', 'NoEndDate' => 'recur_noenddate', - 'PatternEndDate' => 'recur_enddate', - 'Occurrences' => '', + 'PatternEndDate' => 'recur_enddate', + 'Occurrences' => '', ); - // the calendar event array + /** + * the calendar event array for the XML Parser + */ var $event; - // device specific settings + /** + * name and sorftware version of the Funambol client + * + * @var string + */ var $productName = 'mozilla plugin'; var $productSoftwareVersion = '0.3'; + + /** + * user preference: import all-day events as non blocking + * + * @var boolean + */ + var $nonBlockingAllday = false; + + /** + * user preference: attach UID entries to the DESCRIPTION + * + * @var boolean + */ var $uidExtension = false; + /** + * user preference: calendar to synchronize with + * + * @var int + */ + var $calendarOwner = 0; + + /** + * user preference: use server timezone for exports to device + * + * @var boolean + */ + var $useServerTZ = false; + + /** + * Device CTCap Properties + * + * @var array + */ + var $clientProperties; + + /** + * vCalendar Instance for parsing + * + * @var array + */ + var $vCalendar; + + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + var $logfile="/tmp/log-sifcal"; + + // constants for recurence type - const olRecursDaily = 0; + const olRecursDaily = 0; const olRecursWeekly = 1; const olRecursMonthly = 2; const olRecursMonthNth = 3; @@ -72,28 +128,50 @@ class calendar_sif extends calendar_boupdate const olRecursYearNth = 6; // constants for weekdays - const olSunday = 1; - const olMonday = 2; - const olTuesday = 4; - const olWednesday = 8; - const olThursday = 16; - const olFriday = 32; - const olSaturday = 64; + const olSunday = 1; + const olMonday = 2; + const olTuesday = 4; + const olWednesday = 8; + const olThursday = 16; + const olFriday = 32; + const olSaturday = 64; // standard headers const xml_decl = ''; const SIF_decl = '1.1'; + + /** + * Constructor + * + * @param array $_clientProperties client properties + */ + function __construct(&$_clientProperties = array()) + { + parent::__construct(); + if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-sifcal"; + $this->clientProperties = $_clientProperties; + $this->vCalendar = new Horde_iCalendar; + } + + function startElement($_parser, $_tag, $_attributes) { } function endElement($_parser, $_tag) { - //error_log('endElem: ' . $_tag .' => '. trim($this->sifData)); - if(!empty($this->sifMapping[$_tag])) + switch (strtolower($_tag)) { - $this->event[$this->sifMapping[$_tag]] = trim($this->sifData); + case 'excludedate': + $this->event['recur_exception'][] = trim($this->sifData); + break; + + default: + if(!empty($this->sifMapping[$_tag])) + { + $this->event[$this->sifMapping[$_tag]] = trim($this->sifData); + } } unset($this->sifData); } @@ -103,18 +181,27 @@ class calendar_sif extends calendar_boupdate $this->sifData .= $_data; } + /** + * Get DateTime value for a given time and timezone + * + * @param int|string|DateTime $time in server-time as returned by calendar_bo for $data_format='server' + * @param boolean $utc=true if true, return timespamps in UTC, else in server time + * @return mixed attribute value to set: integer timestamp if $tzid == 'UTC' otherwise Ymd\THis string IN $tzid + */ + function getDateTime($time, $utc=true) + { + if ($utc) + { + return $this->vCalendar->_exportDateTime($time); + } + return date('Ymd\THis', $time); + } + function siftoegw($sifData, $_calID=-1) { - $vcal = new Horde_iCalendar; $finalEvent = array(); + $this->event = array(); $sysCharSet = $GLOBALS['egw']->translation->charset(); - #error_log($sifData); - - #$tmpfname = tempnam('/tmp/sync/contents','sife_'); - - #$handle = fopen($tmpfname, "w"); - #fwrite($handle, $sifData); - #fclose($handle); $this->xml_parser = xml_parser_create('UTF-8'); xml_set_object($this->xml_parser, $this); @@ -127,15 +214,21 @@ class calendar_sif extends calendar_boupdate error_log(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($this->xml_parser)), xml_get_current_line_number($this->xml_parser))); + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); return false; } - #error_log(print_r($this->event, true)); foreach ($this->event as $key => $value) { $value = preg_replace('/<\!\[CDATA\[(.+)\]\]>/Usim', '$1', $value); $value = $GLOBALS['egw']->translation->convert($value, 'utf-8', $sysCharSet); - #error_log("$key => $value"); + /* + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() $key => $value\n",3,$this->logfile); + } + */ switch ($key) { case 'alldayevent': @@ -143,17 +236,17 @@ class calendar_sif extends calendar_boupdate { $finalEvent['whole_day'] = true; $startParts = explode('-',$this->event['start']); - $finalEvent['start']['hour'] = $finalEvent['start']['minute'] = $finalEvent['start']['second'] = 0; - $finalEvent['start']['year'] = $startParts[0]; - $finalEvent['start']['month'] = $startParts[1]; - $finalEvent['start']['day'] = $startParts[2]; - $finalEvent['start'] = $this->date2ts($finalEvent['start']); + $finalEvent['startdate']['hour'] = $finalEvent['startdate']['minute'] = $finalEvent['startdate']['second'] = 0; + $finalEvent['startdate']['year'] = $startParts[0]; + $finalEvent['startdate']['month'] = $startParts[1]; + $finalEvent['startdate']['day'] = $startParts[2]; + $finalEvent['start'] = $this->date2ts($finalEvent['startdate']); $endParts = explode('-',$this->event['end']); - $finalEvent['end']['hour'] = 23; $finalEvent['end']['minute'] = $finalEvent['end']['second'] = 59; - $finalEvent['end']['year'] = $endParts[0]; - $finalEvent['end']['month'] = $endParts[1]; - $finalEvent['end']['day'] = $endParts[2]; - $finalEvent['end'] = $this->date2ts($finalEvent['end']); + $finalEvent['enddate']['hour'] = 23; $finalEvent['enddate']['minute'] = $finalEvent['enddate']['second'] = 59; + $finalEvent['enddate']['year'] = $endParts[0]; + $finalEvent['enddate']['month'] = $endParts[1]; + $finalEvent['enddate']['day'] = $endParts[2]; + $finalEvent['end'] = $this->date2ts($finalEvent['enddate']); } break; @@ -167,7 +260,7 @@ class calendar_sif extends calendar_boupdate $categories1 = explode(',', $value); $categories2 = explode(';', $value); $categories = count($categories1) > count($categories2) ? $categories1 : $categories2; - $finalEvent[$key] = implode(',', $this->find_or_add_categories($categories, $_calID)); + $finalEvent[$key] = $this->find_or_add_categories($categories, $_calID); } break; @@ -175,18 +268,31 @@ class calendar_sif extends calendar_boupdate case 'start': if ($this->event['alldayevent'] < 1) { - $finalEvent[$key] = $vcal->_parseDateTime($value); - error_log("event ".$key." val=".$value.", parsed=".$finalEvent[$key]); + $finalEvent[$key] = $this->vCalendar->_parseDateTime($value); } break; case 'isrecurring': if ($value == 1) { + $finalEvent['recur_exception'] = array(); + if (is_array($this->event['recur_exception'])) + { + foreach ($this->event['recur_exception'] as $day) + { + $finalEvent['recur_exception'][] = $this->vCalendar->_parseDateTime($day); + } + array_unique($finalEvent['recur_exception']); + } $finalEvent['recur_interval'] = $this->event['recur_interval']; + $finalEvent['recur_data'] = 0; if ($this->event['recur_noenddate'] == 0) { - $finalEvent['recur_enddate'] = $vcal->_parseDateTime($this->event['recur_enddate']); + $recur_enddate = $this->vCalendar->_parseDateTime($this->event['recur_enddate']); + $finalEvent['recur_enddate'] = mktime(0, 0, 0, + date('m', $recur_enddate), + date('d', $recur_enddate), + date('Y', $recur_enddate)); } switch ($this->event['recur_type']) { @@ -231,6 +337,7 @@ class calendar_sif extends calendar_boupdate case 'recur_interval': case 'recur_weekmask': case 'reminderstart': + case 'recur_exception': // do nothing, get's handled in isrecuring clause break; @@ -241,29 +348,37 @@ class calendar_sif extends calendar_boupdate } default: - $finalEvent[$key] = $value; + $finalEvent[$key] = str_replace("\r\n", "\n", $value); break; } } - #$middleName = ($finalEvent['n_middle']) ? ' '.trim($finalEvent['n_middle']) : ''; - #$finalEvent['fn'] = trim($finalEvent['n_given']. $middleName .' '. $finalEvent['n_family']); + if ($this->calendarOwner) $finalEvent['owner'] = $this->calendarOwner; - #error_log(print_r($finalEvent, true)); + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + + if ($_calID > 0) $finalEvent['id'] = $_calID; + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($finalEvent)."\n",3,$this->logfile); + } return $finalEvent; } function search($_sifdata, $contentID=null, $relax=false) { - $result = false; + $result = array(); + $filter = $relax ? 'relax' : 'exact'; if ($event = $this->siftoegw($_sifdata, $contentID)) { if ($contentID) { $event['id'] = $contentID; } - $result = $this->find_event($event, $relax); + $result = $this->find_event($event, $filter); } return $result; } @@ -273,315 +388,592 @@ class calendar_sif extends calendar_boupdate * @param string $_sifdata the SIFE data * @param int $_calID=-1 the internal addressbook id * @param boolean $merge=false merge data with existing entry + * @param int $recur_date=0 if set, import the recurrence at this timestamp, + * default 0 => import whole series (or events, if not recurring) * @desc import a SIFE into the calendar */ - function addSIF($_sifdata, $_calID=-1, $merge=false) + function addSIF($_sifdata, $_calID=-1, $merge=false, $recur_date=0) { - $state = &$_SESSION['SyncML.state']; - $deviceInfo = $state->getClientDeviceInfo(); - + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_sifdata)."\n",3,$this->logfile); + } if (!$event = $this->siftoegw($_sifdata, $_calID)) { return false; } + + if ($recur_date) $event['reference'] = $recur_date; + $event_info = $this->get_event_info($event); + + // common adjustments for existing events + if (is_array($event_info['stored_event'])) + { + if (empty($event['uid'])) + { + $event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered + } + if ($merge) + { + // overwrite with server data for merge + foreach ($event_info['stored_event'] as $key => $value) + { + if (in_array($key, array('participants', 'participant_types'))) + { + unset($event[$key]); + continue; + } + if (!empty($value)) $event[$key] = $value; + } + } + else + { + // not merge + // SIF clients do not support participants => add them back + unset($event['participants']); + unset($event['participant_types']); + + // avoid that iCal changes the organizer, which is not allowed + $event['owner'] = $event_info['stored_event']['owner']; + } + } + else // common adjustments for new events + { + // set non blocking all day depending on the user setting + if (isset($event['whole_day']) + && $event['whole_day'] + && $this->nonBlockingAllday) + { + $event['non_blocking'] = 1; + } + + // check if an owner is set and the current user has add rights + // for that owners calendar; if not set the current user + if (!isset($event['owner']) + || !$this->check_perms(EGW_ACL_ADD, 0, $event['owner'])) + { + $event['owner'] = $this->user; + } + + $status = $event['owner'] == $this->user ? 'A' : 'U'; + $status = calendar_so::combine_status($status, 1, 'CHAIR'); + $event['participants'] = array($event['owner'] => $status); + } + + unset($event['startdate']); + unset($event['enddate']); + + $alarmData = array(); if (isset($event['alarm'])) { - $alarm = $event['alarm']; - unset($event['alarm']); + $alarmData['offset'] = $event['alarm'] * 60; + $alarmData['time'] = $event['start'] - $alarmData['offset']; + $alarmData['owner'] = $this->user; + $alarmData['all'] = false; } - if ($_calID > 0) + // update alarms depending on the given event type + if (!empty($alarmData) || isset($this->supportedFields['alarm'])) { - // update entry - $event['id'] = $_calID; - } - else - { - if (isset($event['whole_day']) && $event['whole_day'] - && isset ($deviceInfo) && is_array($deviceInfo) - && isset($deviceInfo['nonBlockingAllday']) - && $deviceInfo['nonBlockingAllday']) + switch ($event_info['type']) { - $event['non_blocking'] = '1'; + case 'SINGLE': + case 'SERIES-MASTER': + case 'SERIES-EXCEPTION': + case 'SERIES-EXCEPTION-PROPAGATE': + if (isset($event['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 ($alarmData['time'] == $alarm_data['time'] && + ($alarm_data['all'] || $alarm_data['owner'] == $this->user)) + { + unset($alarmData); + unset($event_info['stored_event']['alarm'][$alarm_id]); + break; + } + } + if (isset($alarmData)) $event['alarm'][] = $alarmData; + } + } + break; + + case 'SERIES-PSEUDO-EXCEPTION': + // 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 ($eventID = $this->update($event, TRUE)) + // save event depending on the given event type + switch ($event_info['type']) { - $updatedEvent = $this->read($eventID); - foreach ($updatedEvent['alarm'] as $alarmID => $alarmData) - { - $this->delete_alarm($alarmID); - } + case 'SINGLE': + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SINGLE\n",3,$this->logfile); + } - if (isset($alarm)) - { - $alarmData['time'] = $event['start'] - ($alarm*60); - $alarmData['offset'] = $alarm*60; - $alarmData['all'] = 1; - $alarmData['owner'] = $GLOBALS['egw_info']['user']['account_id']; - $this->save_alarm($eventID, $alarmData); - } + // update the event + if ($event_info['acl_edit']) + { + // Force SINGLE + $event['reference'] = 0; + $event_to_store = array($event); // prevent $event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $updated_id = $this->update($event_to_store, true); + unset($event_to_store); + } + break; + + case 'SERIES-MASTER': + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-MASTER\n",3,$this->logfile); + } + + // update the event + if ($event_info['acl_edit']) + { + $event_to_store = array($event); // prevent $event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $updated_id = $this->update($event_to_store, true); + unset($event_to_store); + } + break; + + case 'SERIES-EXCEPTION': + case 'SERIES-EXCEPTION-PROPAGATE': + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-EXCEPTION\n",3,$this->logfile); + } + + // update event + if ($event_info['acl_edit']) + { + if (isset($event_info['stored_event']['id'])) + { + // We update an existing exception + $event['id'] = $event_info['stored_event']['id']; + $event['category'] = $event_info['stored_event']['category']; + } + else + { + // We create a new exception + unset($event['id']); + unset($event_info['stored_event']); + $event['recur_type'] = MCAL_RECUR_NONE; + $event_info['master_event']['recur_exception'] = + array_unique(array_merge($event_info['master_event']['recur_exception'], + array($event['reference']))); + $event['category'] = $event_info['master_event']['category']; + $event['owner'] = $event_info['master_event']['owner']; + $event_to_store = array($event_info['master_event']); // prevent the master_event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $this->update($event_to_store, true); + unset($event_to_store); + } + + $event_to_store = array($event); // prevent $event from being changed by update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $updated_id = $this->update($event_to_store, true); + unset($event_to_store); + } + break; + + case 'SERIES-PSEUDO-EXCEPTION': + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-PSEUDO-EXCEPTION\n",3,$this->logfile); + } + + if ($event_info['acl_edit']) + { + // truncate the status only exception from the series master + $recur_exceptions = array(); + foreach ($event_info['master_event']['recur_exception'] as $recur_exception) + { + if ($recur_exception != $event['reference']) + { + $recur_exceptions[] = $recur_exception; + } + } + $event_info['master_event']['recur_exception'] = $recur_exceptions; + + // save the series master with the adjusted exceptions + $event_to_store = array($event_info['master_event']); // prevent the master_event from being changed by the update method + $this->db2data($event_to_store); + $event_to_store = array_shift($event_to_store); + $updated_id = $this->update($event_to_store, true, true, false, false); + unset($event_to_store); + } } - return $eventID; + // read stored event into info array for fresh stored (new) events + if (!is_array($event_info['stored_event']) && $updated_id > 0) + { + $event_info['stored_event'] = $this->read($updated_id); + } + + // choose which id to return to the client + switch ($event_info['type']) + { + case 'SINGLE': + case 'SERIES-MASTER': + case 'SERIES-EXCEPTION': + $return_id = $updated_id; + break; + + case 'SERIES-PSEUDO-EXCEPTION': + $return_id = is_array($event_info['master_event']) ? $event_info['master_event']['id'] . ':' . $event['reference'] : false; + break; + + case 'SERIES-EXCEPTION-PROPAGATE': + if ($event_info['acl_edit'] && is_array($event_info['stored_event'])) + { + // we had sufficient rights to propagate the status only exception to a real one + $return_id = $event_info['stored_event']['id']; + } + else + { + // we did not have sufficient rights to propagate the status only exception to a real one + // we have to keep the SERIES-PSEUDO-EXCEPTION id and keep the event untouched + $return_id = $event_info['master_event']['id'] . ':' . $event['reference']; + } + break; + } + + if ($this->log) + { + $recur_date = $this->date2usertime($event_info['stored_event']['start']); + $event_info['stored_event'] = $this->read($event_info['stored_event']['id'], $recur_date); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event_info['stored_event'])."\n",3,$this->logfile); + } + + return $return_id; } /** * return a sife * * @param int $_id the id of the event + * @param int $recur_date=0 if set export the next recurrence at or after the timestamp, + * default 0 => export whole series (or events, if not recurring) * @return string containing the SIFE */ - function getSIF($_id) + function getSIF($_id, $recur_date=0) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($_id, $recur_date)\n",3,$this->logfile); + } $sysCharSet = $GLOBALS['egw']->translation->charset(); $fields = array_unique(array_values($this->sifMapping)); sort($fields); + $utc = true; - #$event = $this->read($_id,null,false,'server'); - #error_log("FOUND EVENT: ". print_r($event, true)); - - if (($event = $this->read($_id,null,false,'server'))) + if (!($event = $this->read($_id, $recur_date, false, 'server'))) { - - if ($this->uidExtension) + if ($this->read($_id, $recur_date, true, 'server')) { - if (!preg_match('/\[UID:.+\]/m', $event['description'])) + $retval = -1; // Permission denied + if($this->xmlrpc) { - $event['description'] .= "\n[UID:" . $event['uid'] . "]"; + $GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['no_access'], + $GLOBALS['xmlrpcstr']['no_access']); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() User does not have the permission to read event $_id.\n", + 3,$this->logfile); } } - - $vcal = new Horde_iCalendar('1.0'); - - - $sifEvent = self::xml_decl . "\n" . self::SIF_decl; - - foreach ($this->sifMapping as $sifField => $egwField) + else { - if (empty($egwField)) continue; - - #error_log("$sifField => $egwField"); - #error_log('VALUE1: '.$event[$egwField]); - $value = $GLOBALS['egw']->translation->convert($event[$egwField], $sysCharSet, 'utf-8'); - #error_log('VALUE2: '.$value); - - switch ($sifField) + $retval = false; // Entry does not exist + if ($this->log) { - case 'Importance': - $value = $value-1; - $sifEvent .= "<$sifField>$value"; - break; + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Event $_id not found.\n",3,$this->logfile); + } + } + return $retval; + } - case 'RecurrenceType': - case 'Interval': - case 'PatternStartDate': - case 'NoEndDate': - case 'DayOfWeekMask': - case 'PatternEndDate': - break; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } - case 'IsRecurring': - if ($event['recur_type'] == MCAL_RECUR_NONE) + if ($this->isWholeDay($event)) $event['whole_day'] = true; + + if ($recur_date) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($_id, $recur_date) Unsupported status only exception, skipped ...\n", + 3, $this->logfile); + } + return false; // unsupported pseudo exception + } + + if (date('e', $event['start']) != 'UTC' + && ($event['recur_type'] != MCAL_RECUR_NONE + || $this->useServerTZ)) + { + if (!$this->useServerTZ && + $event['recur_type'] != MCAL_RECUR_NONE + && $event['recur_enddate']) + { + $startDST = date('I', $event['start']); + $finalDST = date('I', $event['recur_enddate']); + // Different DST or more than half a year? + if ($startDST != $finalDST || + ($event['recur_enddate'] - $event['start']) > 15778800) + { + $utc = false; + } + } + else + { + $utc = false; + } + } + + if ($this->uidExtension) + { + if (!preg_match('/\[UID:.+\]/m', $event['description'])) + { + $event['description'] .= "\n[UID:" . $event['uid'] . "]"; + } + } + + $sifEvent = self::xml_decl . "" . self::SIF_decl; + + foreach ($this->sifMapping as $sifField => $egwField) + { + if (empty($egwField)) continue; + + $value = $GLOBALS['egw']->translation->convert($event[$egwField], $sysCharSet, 'utf-8'); + + switch ($sifField) + { + case 'Importance': + $value = $value-1; + $sifEvent .= "<$sifField>$value"; + break; + + case 'RecurrenceType': + case 'Interval': + case 'PatternStartDate': + case 'NoEndDate': + case 'DayOfWeekMask': + case 'PatternEndDate': + break; + + case 'IsRecurring': + if ($event['recur_type'] == MCAL_RECUR_NONE) + { + $sifEvent .= "<$sifField>0"; + break; + } + if ($event['recur_enddate'] == 0) + { + $sifEvent .= '1'; + } + else + { + $recurrences = $this->so->get_recurrences($_id); + $occurrences = count($recurrences) + count($event['recur_exception']) - 1; + end($recurrences); + $last = key($recurrences); + if ($last < end($event['recur_exception'])) { - $sifEvent .= "<$sifField>0"; - break; + $last = end($event['recur_exception']); } - if ($event['recur_enddate'] == 0) - { - $sifEvent .= '1'; - } - else - { - $recurEndDate = mktime(24 , 0, 0, - date('m',$event['recur_enddate']), - date('d', $event['recur_enddate']), - date('Y', $event['recur_enddate'])); - $sifEvent .= '0'; - $sifEvent .= ''. $vcal->_exportDateTime($recurEndDate) .''; - } - switch ($event['recur_type']) - { + $recurEndDate = $last - $event['start'] + $event['end']; + $sifEvent .= '0'; + $sifEvent .= ''. self::getDateTime($recurEndDate,$utc) .''; + } - case MCAL_RECUR_DAILY: - $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); - $recurStartDate = mktime(0,0,0,date('m',$event['start']), date('d', $event['start']), date('Y', $event['start'])); + $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursDaily .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $vcal->_exportDateTime($recurStartDate) .''; - if ($event['recur_enddate']) - { - $totalDays = ($recurEndDate - $recurStartDate) / 86400; - $occurrences = ceil($totalDays / $eventInterval); - $sifEvent .= ''. $occurrences .''; - } - break; + switch ($event['recur_type']) + { - case MCAL_RECUR_WEEKLY: - $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); - $recurStartDate = mktime(0,0,0,date('m',$event['start']), date('d', $event['start']), date('Y', $event['start'])); - - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursWeekly .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $vcal->_exportDateTime($recurStartDate) .''; - $sifEvent .= ''. $event['recur_data'] .''; - if ($event['recur_enddate']) - { - $daysPerWeek = substr_count(decbin($event['recur_data']),'1'); - $totalWeeks = floor(($recurEndDate - $recurStartDate) / (86400*7)); - #error_log("AAA: $daysPerWeek $totalWeeks"); - $occurrences = ($totalWeeks / $eventInterval) * $daysPerWeek; - for($i = $recurEndDate; $i > $recurStartDate + ($totalWeeks * 86400*7); $i = $i - 86400) - { - switch (date('w', $i-1)) - { - case 0: - if ($event['recur_data'] & 1) $occurrences++; - break; - // monday - case 1: - if ($event['recur_data'] & 2) $occurrences++; - break; - case 2: - if ($event['recur_data'] & 4) $occurrences++; - break; - case 3: - if ($event['recur_data'] & 8) $occurrences++; - break; - case 4: - if ($event['recur_data'] & 16) $occurrences++; - break; - case 5: - if ($event['recur_data'] & 32) $occurrences++; - break; - case 6: - if ($event['recur_data'] & 64) $occurrences++; - break; - } - } - $sifEvent .= ''. $occurrences .''; - } - break; - - case MCAL_RECUR_MONTHLY_MDAY: - $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); - $recurStartDate = mktime(0,0,0,date('m',$event['start']), date('d', $event['start']), date('Y', $event['start'])); - - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursMonthly .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $vcal->_exportDateTime($recurStartDate) .''; - break; - - case MCAL_RECUR_MONTHLY_WDAY: - $weekMaskMap = array('Sun' => self::olSunday, 'Mon' => self::olMonday, 'Tue' => self::olTuesday, - 'Wed' => self::olWednesday, 'Thu' => self::olThursday, 'Fri' => self::olFriday, - 'Sat' => self::olSaturday); - $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); - $recurStartDate = mktime(0,0,0,date('m',$event['start']), date('d', $event['start']), date('Y', $event['start'])); - - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursMonthNth .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $vcal->_exportDateTime($recurStartDate) .''; - $sifEvent .= '' . (1 + (int) ((date('d',$event['start'])-1) / 7)) . ''; - $sifEvent .= '' . $weekMaskMap[date('D',$event['start'])] . ''; - break; - - case MCAL_RECUR_YEARLY: - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursYearly .''; - break; - } - break; - - case 'Sensitivity': - $value = (!$value ? '2' : '0'); - $sifEvent .= "<$sifField>$value"; - break; - - case 'Folder': - # skip currently. This is the folder where Outlook stores the contact. - #$sifEvent .= "<$sifField>/"; - break; - - case 'AllDayEvent': - case 'End': - // get's handled by Start clause - break; - - case 'Start': - if ($this->isWholeDay($event)) - { - $value = date('Y-m-d', $event['start']); - $sifEvent .= "$value"; - $vaule = date('Y-m-d', $event['end']); - $sifEvent .= "$value"; - $sifEvent .= "1"; - } - else - { - $value = $vcal->_exportDateTime($event['start']); - $sifEvent .= "$value"; - $value = $vcal->_exportDateTime($event['end']); - $sifEvent .= "$value"; - $sifEvent .= "0"; - } - break; - - case 'ReminderMinutesBeforeStart': - break; - - case 'ReminderSet': - if (count((array)$event['alarm']) > 0) - { + case MCAL_RECUR_DAILY: $sifEvent .= "<$sifField>1"; - foreach ($event['alarm'] as $alarmID => $alarmData) + $sifEvent .= ''. self::olRecursDaily .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. self::getDateTime($event['start'],$utc) .''; + if ($event['recur_enddate']) { - $sifEvent .= ''. $alarmData['offset']/60 .''; - // lets take only the first alarm - break; + $sifEvent .= ''. $occurrences .''; + } + break; + + case MCAL_RECUR_WEEKLY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursWeekly .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. self::getDateTime($event['start'],$utc) .''; + $sifEvent .= ''. $event['recur_data'] .''; + if ($event['recur_enddate']) + { + $sifEvent .= ''. $occurrences .''; + } + break; + + case MCAL_RECUR_MONTHLY_MDAY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursMonthly .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. self::getDateTime($event['start'],$utc) .''; + break; + + case MCAL_RECUR_MONTHLY_WDAY: + $weekMaskMap = array('Sun' => self::olSunday, 'Mon' => self::olMonday, 'Tue' => self::olTuesday, + 'Wed' => self::olWednesday, 'Thu' => self::olThursday, 'Fri' => self::olFriday, + 'Sat' => self::olSaturday); + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursMonthNth .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. self::getDateTime($event['start'],$utc) .''; + $sifEvent .= '' . (1 + (int) ((date('d',$event['start'])-1) / 7)) . ''; + $sifEvent .= '' . $weekMaskMap[date('D',$event['start'])] . ''; + break; + + case MCAL_RECUR_YEARLY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursYearly .''; + break; + } + if (is_array($event['recur_exception'])) + { + $sifEvent .= ''; + foreach ($event['recur_exception'] as $day) + { + if (isset($event['whole_day'])) + { + $sifEvent .= '' . date('Y-m-d', $day) . ''; + } + else + { + $sifEvent .= '' . self::getDateTime($day,$utc) . ''; } } - else - { - $sifEvent .= "<$sifField>0"; - } - break; + $sifEvent .= ''; + } + break; - case 'Categories': - if (!empty($value)) - { - $value = implode(', ', $this->get_categories($value)); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - else + case 'Sensitivity': + $value = (!$value ? '2' : '0'); + $sifEvent .= "<$sifField>$value"; + break; + + case 'Folder': + # skip currently. This is the folder where Outlook stores the contact. + #$sifEvent .= "<$sifField>/"; + break; + + case 'AllDayEvent': + case 'End': + // get's handled by Start clause + break; + + case 'Start': + if (isset($event['whole_day'])) + { + $sifEvent .= '' . date('Y-m-d', $event['start']) . ''; + $sifEvent .= '' . date('Y-m-d', $event['end']) . ''; + $sifEvent .= "1"; + } + else + { + $sifEvent .= '' . self::getDateTime($event['start'],$utc) . ''; + $sifEvent .= '' . self::getDateTime($event['end'],$utc) . ''; + $sifEvent .= "0"; + } + break; + + case 'ReminderMinutesBeforeStart': + break; + + case 'ReminderSet': + if (count((array)$event['alarm']) > 0) + { + $sifEvent .= "<$sifField>1"; + foreach ($event['alarm'] as $alarmID => $alarmData) { + $sifEvent .= ''. $alarmData['offset']/60 .''; + // lets take only the first alarm break; } + } + else + { + $sifEvent .= "<$sifField>0"; + } + break; - default: - $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); - $sifEvent .= "<$sifField>$value"; + case 'Categories': + if (!empty($value) && ($values = $this->get_categories($value))) + { + $value = implode(', ', $values); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } + else + { break; - } + } + + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); + $sifEvent .= "<$sifField>$value"; } - $sifEvent .= ''; - - return $sifEvent; } + $sifEvent .= ""; - if($this->xmlrpc) + if ($this->log) { - $GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['no_access'],$GLOBALS['xmlrpcstr']['no_access']); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() '$this->productName','$this->productSoftwareVersion'\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "()\n".array2string($sifEvent)."\n",3,$this->logfile); } - return False; + + return $sifEvent; } /** @@ -594,18 +986,43 @@ class calendar_sif extends calendar_boupdate */ function setSupportedFields($_productName='', $_productSoftwareVersion='') { - $state = &$_SESSION['SyncML.state']; - $deviceInfo = $state->getClientDeviceInfo(); + $state =& $_SESSION['SyncML.state']; + if (isset($state)) + { + $deviceInfo = $state->getClientDeviceInfo(); + } if (isset($deviceInfo) && is_array($deviceInfo)) { if (isset($deviceInfo['uidExtension']) && - $deviceInfo['uidExtension']) + $deviceInfo['uidExtension']) + { + $this->uidExtension = true; + } + if (isset($deviceInfo['nonBlockingAllday']) && + $deviceInfo['nonBlockingAllday']) + { + $this->nonBlockingAllday = true; + } + if (isset($deviceInfo['tzid']) && + $deviceInfo['tzid']) + { + $this->useServerTZ = ($deviceInfo['tzid'] == 1); + } + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner'])) + { + $owner = $GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner']; + if ($owner == 0) { - $this->uidExtension = true; + $owner = $GLOBALS['egw_info']['user']['account_primary_group']; } + if (0 < (int)$owner && $this->check_perms(EGW_ACL_EDIT,0,$owner)) + { + $this->calendarOwner = $owner; + } + } } - // store product name and version, to be able to use it elsewhere + // store product name and software version for futher usage if ($_productName) { $this->productName = strtolower($_productName); @@ -614,5 +1031,11 @@ class calendar_sif extends calendar_boupdate $this->productSoftwareVersion = $matches[1]; } } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $this->productName . + ', '. $this->productSoftwareVersion . ")\n",3,$this->logfile); + } } } diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index 4e7060b7e1..cab232f19a 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -44,6 +44,7 @@ define('REJECTED',0); define('NO_RESPONSE',1); define('TENTATIVE',2); define('ACCEPTED',3); +define('DELEGATED',4); define('HOUR_s',60*60); define('DAY_s',24*HOUR_s); @@ -112,7 +113,7 @@ class calendar_so * All times (start, end and modified) are returned as timesstamps in servertime! * * @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 recurrance at or after the timestamp, default 0 = read the initital one + * @param int $recur_date=0 if set read the next recurrence at or after the timestamp, default 0 = read the initital one * @return array|boolean array with id => data pairs or false if entry not found */ function read($ids,$recur_date=0) @@ -179,6 +180,18 @@ class calendar_so $this->db->update($this->cal_table, array('cal_uid' => $event['uid']), array('cal_id' => $event['id']),__LINE__,__FILE__,'calendar'); } + if ((int) $recur_date == 0 && + $event['recur_type'] != MCAL_RECUR_NONE && + !empty($event['recur_exception'])) + { + sort($event['recur_exception']); + if ($event['recur_exception'][0] < $event['start']) + { + // leading exceptions => move start and end + $event['end'] -= $event['start'] - $event['recur_exception'][0]; + $event['start'] = $event['recur_exception'][0]; + } + } } // check if we have a real recurance, if not set $recur_date=0 @@ -231,6 +244,37 @@ class calendar_so return $events; } + /** + * Get maximum modification time of participant data of given event(s) + * + * This includes ALL recurences of an event series + * + * @param int|array $ids one or multiple cal_id's + * @return int|array (array of) modification timestamp(s) + */ + function max_user_modified($ids) + { + $etags = array(); + if (is_array($ids)) + { + $events = $ids; + } + else + { + $events = array($ids); + } + foreach ($events as $id) + { + if (!($ts = $GLOBALS['egw']->contenthistory->getTSforAction('calendar', $id, 'modify'))) + { + $ts = $GLOBALS['egw']->contenthistory->getTSforAction('calendar', $id, 'add'); + } + if ($ts) $etags[$id] = $ts; + } + //echo "

".__METHOD__.'('.array2string($ids).') = '.array2string($etags)."

\n"; + return is_array($ids) ? $etags : $etags[$ids]; + } + /** * generate SQL to filter after a given category (evtl. incl. subcategories) * @@ -285,7 +329,7 @@ class calendar_so * * ToDo: search custom-fields too */ - function &search($start,$end,$users,$cat_id=0,$filter='',$query='',$offset=False,$num_rows=0,$order='cal_start',$show_rejected=true,$_cols=null,$append='') + function &search($start,$end,$users,$cat_id=0,$filter='',$query='',$offset=False,$num_rows=0,$order='cal_start',$show_rejected=true,$_cols=null,$append='',$cfs=null) { //echo '

'.__METHOD__.'('.($start ? date('Y-m-d H:i',$start) : '').','.($end ? date('Y-m-d H:i',$end) : '').','.array2string($users).','.array2string($cat_id).",'$filter',".array2string($query).",$offset,$num_rows,$order,$show_rejected,".array2string($_cols).",$append,".array2string($cfs).")

\n"; @@ -326,6 +370,11 @@ class calendar_so 'cal_user_type' => $type, 'cal_user_id' => $ids, )); + if ($type == 'u' && $show_rejected) + { + $cal_table_def = $this->db->get_table_definitions('calendar',$this->cal_table); + $to_or[] = $this->db->expression($cal_table_def,array('cal_owner' => $ids)); + } } $where[] = '('.implode(' OR ',$to_or).')'; @@ -338,7 +387,7 @@ class calendar_so if ($start) $where[] = (int)$start.' < cal_end'; if ($end) $where[] = 'cal_start < '.(int)$end; - if (!preg_match('/^[a-z_ ,]+$/i',$order)) $order = 'cal_start'; // gard against SQL injunktion + if (!preg_match('/^[a-z_ ,]+$/i',$order)) $order = 'cal_start'; // gard against SQL injection if ($this->db->capabilities['distinct_on_text'] && $this->db->capabilities['union']) { @@ -390,12 +439,24 @@ class calendar_so $events = $ids = $recur_dates = $recur_ids = array(); foreach($rs as $row) { - $ids[] = $id = $row['cal_id']; + $id = $row['cal_id']; + if (is_numeric($id)) $ids[] = $id; + if ($row['cal_recur_date']) { $id .= '-'.$row['cal_recur_date']; $recur_dates[] = $row['cal_recur_date']; } + if ($row['participants']) + { + $row['participants'] = explode(',',$row['participants']); + $row['participants'] = array_combine($row['participants'], + array_fill(0,count($row['participants']),'')); + } + else + { + $row['participants'] = array(); + } $row['alarm'] = array(); $row['recur_exception'] = $row['recur_exception'] ? explode(',',$row['recur_exception']) : array(); @@ -523,6 +584,8 @@ ORDER BY cal_user_type, cal_usre_id $minimum_uid_length = 8; } + $old_min = $old_duration = 0; + //echo '

'.__METHOD__.'('.array2string($event).",$change_since) event="; _debug_array($event); //error_log(__METHOD__.'('.array2string($event).",$set_recurrences,$change_since,$etag)"); @@ -545,7 +608,19 @@ ORDER BY cal_user_type, cal_usre_id unset($event[$col]); } } - if (is_array($event['cal_category'])) $event['cal_category'] = implode(',',$event['cal_category']); + // ensure that we find mathing entries later on + if (!is_array($event['cal_category'])) + { + $categories = array_unique(explode(',',$event['cal_category'])); + sort($categories); + } + else + { + $categories = array_unique($event['cal_category']); + } + sort($categories, SORT_NUMERIC); + + $event['cal_category'] = implode(',',$categories); if ($cal_id) { @@ -684,7 +759,7 @@ ORDER BY cal_user_type, cal_usre_id // update start- and endtime if present in the event-array, evtl. we need to move all recurrences if (isset($event['cal_start']) && isset($event['cal_end'])) { - $this->move($cal_id,$event['cal_start'],$event['cal_end'],!$cal_id ? false : $change_since); + $this->move($cal_id,$event['cal_start'],$event['cal_end'],!$cal_id ? false : $change_since, $old_min, $old_min + $old_duration); } // update participants if present in the event-array if (isset($event['cal_participants'])) @@ -860,7 +935,7 @@ ORDER BY cal_user_type, cal_usre_id /** * splits the combined status, quantity and role * - * @param string &$status I: combined value, O: status letter: U, T, A, R + * @param string &$status I: combined value, O: status letter: U, T, A, R, D * @param int &$quantity only O: quantity * @param string &$role only O: role */ @@ -875,6 +950,10 @@ ORDER BY cal_user_type, cal_usre_id if ($matches[2]) $role = $matches[2]; $status = $status[0]; } + elseif ($status === true) + { + $status = 'U'; + } } /** @@ -1009,7 +1088,8 @@ ORDER BY cal_user_type, cal_usre_id REJECTED => 'R', NO_RESPONSE => 'U', TENTATIVE => 'T', - ACCEPTED => 'A' + ACCEPTED => 'A', + DELEGATED => 'D' ); if (!(int)$cal_id || !(int)$user_id && $user_type != 'e') { @@ -1323,16 +1403,14 @@ ORDER BY cal_user_type, cal_usre_id * get stati of all recurrences of an event for a specific participant * * @param int $cal_id - * @param int $uid participant uid + * @param int $uid=null participant uid; if == null return onyl the recur dates * @param int $start=0 if != 0: startdate of the search/list (servertime) * @param int $end=0 if != 0: enddate of the search/list (servertime) * * @return array recur_date => status pairs (index 0 => main status) */ - function get_recurrences($cal_id, $uid, $start=0, $end=0) + function get_recurrences($cal_id, $uid=null, $start=0, $end=0) { - $user_type = $user_id = null; - self::split_user($uid, $user_type, $user_id); $participant_status = array(); $where = array('cal_id' => $cal_id); if ($start != 0 && $end == 0) $where[] = '(cal_recur_date = 0 OR cal_recur_date >= ' . (int)$start . ')'; @@ -1347,6 +1425,9 @@ ORDER BY cal_user_type, cal_usre_id // inititalize the array $participant_status[$row['cal_recur_date']] = null; } + if (is_null($uid)) return $participant_status; + $user_type = $user_id = null; + self::split_user($uid, $user_type, $user_id); $where = array( 'cal_id' => $cal_id, 'cal_user_type' => $user_type ? $user_type : 'u', @@ -1426,21 +1507,21 @@ ORDER BY cal_user_type, cal_usre_id * irregular participant stati * * @param array $event Recurring Event. - * @param int servertime=0 == 0 -> export event with UTC timestamps - * != 0 -> export with servertime timestamps * @param int $start=0 if != 0: startdate of the search/list (servertime) * @param int $end=0 if != 0: enddate of the search/list (servertime) + * @param boolean $show_rejected=true should the search return rejected invitations * * @return array Array of exception days (false for non-recurring events). */ - function get_recurrence_exceptions(&$event, $servertime=0, $start=0, $end=0) + function get_recurrence_exceptions(&$event, $start=0, $end=0, $show_rejected=true) { $cal_id = (int) $event['id']; + $user = $GLOBALS['egw_info']['user']['account_id']; if (!$cal_id || $event['recur_type'] == MCAL_RECUR_NONE) return false; - $days = array(); + $days = $removed_days = array(); - $participants = $this->get_participants($event['id'], 0); + $participants = $this->get_participants($cal_id, 0); // Check if the stati for all participants are identical for all recurrences foreach ($participants as $uid => $attendee) @@ -1450,13 +1531,18 @@ ORDER BY cal_user_type, cal_usre_id case 'u': // account case 'c': // contact case 'e': // email address - $recurrences = $this->get_recurrences($event['id'], $uid, $start, $end); + $recurrences = $this->get_recurrences($cal_id, $uid, $start, $end); foreach ($recurrences as $recur_date => $recur_status) { + if ($uid == $user && !$show_rejected && $recur_status[0] == 'R') + { + $removed_days[$recur_date] = $recur_date; + continue; + } if ($recur_date && $recur_status != $recurrences[0]) { // Every distinct status results in an exception - $days[] = $recur_date; + $days[$recur_date] = $recur_date; } } break; @@ -1464,6 +1550,7 @@ ORDER BY cal_user_type, cal_usre_id break; } } + $days = array_diff($days, $removed_days); $days = array_unique($days); sort($days); return $days; diff --git a/calendar/templates/default/images/forward.gif b/calendar/templates/default/images/forward.gif new file mode 100644 index 0000000000..7a5df0b757 Binary files /dev/null and b/calendar/templates/default/images/forward.gif differ diff --git a/calendar/templates/default/images/forward.png b/calendar/templates/default/images/forward.png new file mode 100644 index 0000000000..7986c31f45 Binary files /dev/null and b/calendar/templates/default/images/forward.png differ diff --git a/phpgwapi/inc/horde/Horde/iCalendar.php b/phpgwapi/inc/horde/Horde/iCalendar.php index f6e99fd9df..6c5454d473 100644 --- a/phpgwapi/inc/horde/Horde/iCalendar.php +++ b/phpgwapi/inc/horde/Horde/iCalendar.php @@ -1222,7 +1222,7 @@ class Horde_iCalendar { $attr_string = $name . $params_str; if (strlen($value) > 0) { $attr_string .= ':' . $value; - } elseif ($name != 'RRULE') { + } elseif ($name != 'RRULE' && $name != 'ATTENDEE' && $name != 'ORGANIZER') { $attr_string .= ':'; } if (!$this->isOldFormat()) {