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,12 +588,20 @@ 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']));
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'];
$events = array();
@ -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 (isset($event['reference']))
{
$recur_date = $event['reference'];
}
if ($event['uid'] && ($uidmatch = $this->read($event['uid'])))
elseif (isset($event['start']))
{
if ($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
}
}
else
{
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)
{
return $found['id'];
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
'(' . $event['id'] . ")[EventID]");
}
if (($egwEvent = $this->read($event['id'], $recur_date, false, 'server')))
{
if ($this->log)
{
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)
{
$event = array_shift($foundEvents);
return $event['id'];
$user = trim($user);
if ($this->check_perms(EGW_ACL_READ|EGW_ACL_READ_FOR_PARTICIPANTS|EGW_ACL_FREEBUSY,0,$user))
{
if ($user && !in_array($user,$users)) // already added?
{
$users[] = $user;
}
}
return false;
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'];
}
}
}
}
}
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

View File

@ -55,14 +55,70 @@ class calendar_sif extends calendar_boupdate
'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 olRecursWeekly = 1;
@ -84,17 +140,39 @@ class calendar_sif extends calendar_boupdate
const xml_decl = '<?xml version="1.0" encoding="UTF-8"?>';
const SIF_decl = '<SIFVersion>1.1</SIFVersion>';
/**
* 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));
switch (strtolower($_tag))
{
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,79 +388,388 @@ 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 (isset($event['alarm']))
{
$alarm = $event['alarm'];
unset($event['alarm']);
}
if ($_calID > 0)
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']))
{
// update entry
$event['id'] = $_calID;
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
{
if (isset($event['whole_day']) && $event['whole_day']
&& isset ($deviceInfo) && is_array($deviceInfo)
&& isset($deviceInfo['nonBlockingAllday'])
&& $deviceInfo['nonBlockingAllday'])
// 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
{
$event['non_blocking'] = '1';
// 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']))
{
$alarmData['offset'] = $event['alarm'] * 60;
$alarmData['time'] = $event['start'] - $alarmData['offset'];
$alarmData['owner'] = $this->user;
$alarmData['all'] = false;
}
// update alarms depending on the given event type
if (!empty($alarmData) || isset($this->supportedFields['alarm']))
{
switch ($event_info['type'])
{
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)
case 'SINGLE':
if ($this->log)
{
$this->delete_alarm($alarmID);
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
"(): event SINGLE\n",3,$this->logfile);
}
if (isset($alarm))
// update the event
if ($event_info['acl_edit'])
{
$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);
// 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->read($_id, $recur_date, true, 'server'))
{
$retval = -1; // Permission denied
if($this->xmlrpc)
{
$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);
}
}
else
{
$retval = false; // Entry does not exist
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
"() Event $_id not found.\n",3,$this->logfile);
}
}
return $retval;
}
if ($this->log)
{
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
array2string($event)."\n",3,$this->logfile);
}
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)
{
@ -355,19 +779,13 @@ class calendar_sif extends calendar_boupdate
}
}
$vcal = new Horde_iCalendar('1.0');
$sifEvent = self::xml_decl . "\n<appointment>" . self::SIF_decl;
$sifEvent = self::xml_decl . "<appointment>" . self::SIF_decl;
foreach ($this->sifMapping as $sifField => $egwField)
{
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)
{
@ -396,100 +814,62 @@ class calendar_sif extends calendar_boupdate
}
else
{
$recurEndDate = mktime(24 , 0, 0,
date('m',$event['recur_enddate']),
date('d', $event['recur_enddate']),
date('Y', $event['recur_enddate']));
$sifEvent .= '<NoEndDate>0</NoEndDate>';
$sifEvent .= '<PatternEndDate>'. $vcal->_exportDateTime($recurEndDate) .'</PatternEndDate>';
$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']))
{
$last = end($event['recur_exception']);
}
$recurEndDate = $last - $event['start'] + $event['end'];
$sifEvent .= '<NoEndDate>0</NoEndDate>';
$sifEvent .= '<PatternEndDate>'. self::getDateTime($recurEndDate,$utc) .'</PatternEndDate>';
}
$eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1);
switch ($event['recur_type'])
{
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']));
$sifEvent .= "<$sifField>1</$sifField>";
$sifEvent .= '<RecurrenceType>'. self::olRecursDaily .'</RecurrenceType>';
$sifEvent .= '<Interval>'. $eventInterval .'</Interval>';
$sifEvent .= '<PatternStartDate>'. $vcal->_exportDateTime($recurStartDate) .'</PatternStartDate>';
$sifEvent .= '<PatternStartDate>'. self::getDateTime($event['start'],$utc) .'</PatternStartDate>';
if ($event['recur_enddate'])
{
$totalDays = ($recurEndDate - $recurStartDate) / 86400;
$occurrences = ceil($totalDays / $eventInterval);
$sifEvent .= '<Occurrences>'. $occurrences .'</Occurrences>';
}
break;
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</$sifField>";
$sifEvent .= '<RecurrenceType>'. self::olRecursWeekly .'</RecurrenceType>';
$sifEvent .= '<Interval>'. $eventInterval .'</Interval>';
$sifEvent .= '<PatternStartDate>'. $vcal->_exportDateTime($recurStartDate) .'</PatternStartDate>';
$sifEvent .= '<PatternStartDate>'. self::getDateTime($event['start'],$utc) .'</PatternStartDate>';
$sifEvent .= '<DayOfWeekMask>'. $event['recur_data'] .'</DayOfWeekMask>';
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>'. $occurrences .'</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</$sifField>";
$sifEvent .= '<RecurrenceType>'. self::olRecursMonthly .'</RecurrenceType>';
$sifEvent .= '<Interval>'. $eventInterval .'</Interval>';
$sifEvent .= '<PatternStartDate>'. $vcal->_exportDateTime($recurStartDate) .'</PatternStartDate>';
$sifEvent .= '<PatternStartDate>'. self::getDateTime($event['start'],$utc) .'</PatternStartDate>';
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</$sifField>";
$sifEvent .= '<RecurrenceType>'. self::olRecursMonthNth .'</RecurrenceType>';
$sifEvent .= '<Interval>'. $eventInterval .'</Interval>';
$sifEvent .= '<PatternStartDate>'. $vcal->_exportDateTime($recurStartDate) .'</PatternStartDate>';
$sifEvent .= '<PatternStartDate>'. self::getDateTime($event['start'],$utc) .'</PatternStartDate>';
$sifEvent .= '<Instance>' . (1 + (int) ((date('d',$event['start'])-1) / 7)) . '</Instance>';
$sifEvent .= '<DayOfWeekMask>' . $weekMaskMap[date('D',$event['start'])] . '</DayOfWeekMask>';
break;
@ -499,6 +879,22 @@ class calendar_sif extends calendar_boupdate
$sifEvent .= '<RecurrenceType>'. self::olRecursYearly .'</RecurrenceType>';
break;
}
if (is_array($event['recur_exception']))
{
$sifEvent .= '<Exceptions>';
foreach ($event['recur_exception'] as $day)
{
if (isset($event['whole_day']))
{
$sifEvent .= '<ExcludeDate>' . date('Y-m-d', $day) . '</ExcludeDate>';
}
else
{
$sifEvent .= '<ExcludeDate>' . self::getDateTime($day,$utc) . '</ExcludeDate>';
}
}
$sifEvent .= '</Exceptions>';
}
break;
case 'Sensitivity':
@ -517,20 +913,16 @@ class calendar_sif extends calendar_boupdate
break;
case 'Start':
if ($this->isWholeDay($event))
if (isset($event['whole_day']))
{
$value = date('Y-m-d', $event['start']);
$sifEvent .= "<Start>$value</Start>";
$vaule = date('Y-m-d', $event['end']);
$sifEvent .= "<End>$value</End>";
$sifEvent .= '<Start>' . date('Y-m-d', $event['start']) . '</Start>';
$sifEvent .= '<End>' . date('Y-m-d', $event['end']) . '</End>';
$sifEvent .= "<AllDayEvent>1</AllDayEvent>";
}
else
{
$value = $vcal->_exportDateTime($event['start']);
$sifEvent .= "<Start>$value</Start>";
$value = $vcal->_exportDateTime($event['end']);
$sifEvent .= "<End>$value</End>";
$sifEvent .= '<Start>' . self::getDateTime($event['start'],$utc) . '</Start>';
$sifEvent .= '<End>' . self::getDateTime($event['end'],$utc) . '</End>';
$sifEvent .= "<AllDayEvent>0</AllDayEvent>";
}
break;
@ -556,9 +948,9 @@ class calendar_sif extends calendar_boupdate
break;
case 'Categories':
if (!empty($value))
if (!empty($value) && ($values = $this->get_categories($value)))
{
$value = implode(', ', $this->get_categories($value));
$value = implode(', ', $values);
$value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8');
}
else
@ -569,21 +961,21 @@ class calendar_sif extends calendar_boupdate
default:
$value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8');
$sifEvent .= "<$sifField>$value</$sifField>";
break;
}
}
$sifEvent .= '</appointment>';
$sifEvent .= "</appointment>";
if ($this->log)
{
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 $sifEvent;
}
if($this->xmlrpc)
{
$GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['no_access'],$GLOBALS['xmlrpcstr']['no_access']);
}
return False;
}
/**
* Set the supported fields
*
@ -594,8 +986,11 @@ class calendar_sif extends calendar_boupdate
*/
function setSupportedFields($_productName='', $_productSoftwareVersion='')
{
$state = &$_SESSION['SyncML.state'];
$state =& $_SESSION['SyncML.state'];
if (isset($state))
{
$deviceInfo = $state->getClientDeviceInfo();
}
if (isset($deviceInfo) && is_array($deviceInfo))
{
@ -604,8 +999,30 @@ class calendar_sif extends calendar_boupdate
{
$this->uidExtension = true;
}
if (isset($deviceInfo['nonBlockingAllday']) &&
$deviceInfo['nonBlockingAllday'])
{
$this->nonBlockingAllday = true;
}
// store product name and version, to be able to use it elsewhere
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)
{
$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 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);
}
}
}

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