Calendar synchronization backport

This commit is contained in:
Jörg Lehrke 2010-02-23 18:35:43 +00:00
parent 79a2c95678
commit 6433df94ec
9 changed files with 2484 additions and 922 deletions

View File

@ -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;

View File

@ -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,
);
}
}

View File

@ -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.'"';
}
/**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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 "<p>".__METHOD__.'('.array2string($ids).') = '.array2string($etags)."</p>\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 '<p>'.__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).")</p>\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 '<p>'.__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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

View File

@ -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()) {