From 32639bd47e471da37b0aee058bb930a66ce3b673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lehrke?= Date: Fri, 29 Jan 2010 21:42:54 +0000 Subject: [PATCH] Major SyncML Calendar update - SIFE support improved - various vCalendar 1.0 issues fixed - device specific timezone support for recurring events - pseudo exception handling improvements --- calendar/inc/class.calendar_bo.inc.php | 40 +- calendar/inc/class.calendar_boupdate.inc.php | 684 +++++++++-- calendar/inc/class.calendar_ical.inc.php | 717 +++++++---- calendar/inc/class.calendar_rrule.inc.php | 95 +- calendar/inc/class.calendar_sif.inc.php | 1071 ++++++++++++----- calendar/inc/class.calendar_so.inc.php | 427 +++++-- phpgwapi/inc/horde/Horde/SyncML/State.php | 8 +- phpgwapi/inc/horde/Horde/SyncML/State_egw.php | 16 +- .../SyncML/Sync/RefreshFromServerSync.php | 3 + .../inc/horde/Horde/SyncML/Sync/SlowSync.php | 5 +- .../horde/Horde/SyncML/Sync/TwoWaySync.php | 30 +- phpgwapi/inc/horde/Horde/iCalendar.php | 48 +- 12 files changed, 2362 insertions(+), 782 deletions(-) diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index 4c28d7a908..6bb70c11b9 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -525,7 +525,7 @@ class calendar_bo /** * Get integration data for a given app of a part (value for a certain key) of it - * + * * @param string $app * @param string $part * @return array @@ -533,23 +533,23 @@ class calendar_bo static function integration_get_data($app,$part=null) { static $integration_data; - + if (!isset($integration_data)) { $integration_data = calendar_so::get_integration_data(); } - + if (!isset($integration_data[$app])) return null; - + return $part ? $integration_data[$app][$part] : $integration_data[$app]; } - + /** * Get private attribute for an integration event - * + * * Attribute 'is_private' is either a boolean value, eg. false to make all events of $app public * or an ExecMethod callback with parameters $id,$event - * + * * @param string $app * @param int|string $id * @return string @@ -557,7 +557,7 @@ class calendar_bo static function integration_get_private($app,$id,$event) { $app_data = self::integration_get_data($app,'is_private'); - + // no method, fall back to link title if (is_null($app_data)) { @@ -660,22 +660,23 @@ class calendar_bo */ function set_recurrences($event,$start=0) { - if ($this->debug && ((int) $this->debug >= 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont')) + if ($this->debug && ((int) $this->debug >= 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont')) { $this->debug_message('bocal::set_recurrences(%1,%2)',true,$event,$start); } - // check if the caller gave the event start and end times and if not read them from the DB - if (!isset($event['start']) || !isset($event['end'])) - { - $event_read=$this->read($event['id']); - $event['start'] = $event_read['start']; - $event['end'] = $event_read['end']; - } - // 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']; @@ -708,7 +709,6 @@ class calendar_bo foreach($events as $id => &$event) { // convert timezone id of event to tzid (iCal id like 'Europe/Berlin') - unset($event_timezone); if (!$event['tz_id'] || !($event['tzid'] = calendar_timezones::id2tz($event['tz_id']))) { $event['tzid'] = egw_time::$server_timezone->getName(); diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 0e13135d68..59d40d040d 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -61,9 +61,11 @@ class calendar_boupdate extends calendar_bo var $debug; /** - * @var string|boolean $log_file filename to enable the login or false for no update-logging + * Set Logging + * + * @var boolean */ - var $log_file = false; + var $log = false; /** * Constructor @@ -1042,6 +1044,11 @@ class calendar_boupdate extends calendar_bo return false; } calendar_so::split_status($status, $quantity, $role); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($cal_id, $uid, $status, $recur_date)"); + } if (($Ok = $this->so->set_status($cal_id,is_numeric($uid)?'u':$uid[0],is_numeric($uid)?$uid:substr($uid,1),$status,$recur_date ? $this->date2ts($recur_date,true) : 0,$role))) { if ($updateTS) $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id,'modify',time()); @@ -1407,111 +1414,430 @@ 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 -> check for identical matches + * relax -> be more tolerant + * master -> try to find a releated series master + * @return array calendar_id's of matching entries */ - function find_event($event, $relax=false) + function find_event($event, $filter='exact') { + $matchingEvents = array(); $query = array(); - if (isset($event['start'])) + // unset($event['uid']); + + 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]; + $recur_date = 0; + $event['recurrence'] = 0; } - - if ($event['uid'] && ($uidmatch = $this->read($event['uid']))) + else { - if ($event['recurrence']) + if ($event['recur_type'] == MCAL_RECUR_NONE) { - // Let's try to find a real exception first - $query['cal_uid'] = $event['uid']; - $query['cal_recurrence'] = $event['recurrence']; - - if ($foundEvents = parent::search(array( - 'query' => $query, - ))) + if (empty($event['uid']) || !isset($event['recurrence'])) { - 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['recurrence']))) - { - // 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['recurrence']); - - return false; // We need to create a new pseudo exception + $event['recurrence'] = $event['start']; } + $recur_date = $event['recurrence']; } else { - return $uidmatch['id']; + $event['recurrence'] = 0; + $recur_date = $event['start']; } + + $recur_date = $this->date2usertime($recur_date); } - 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 == 'master' && $egwEvent['recur_type'] != MCAL_RECUR_NONE || + $egwEvent['title'] == $event['title'] && + abs($egwEvent['start'] - $event['start']) < 60 && + abs($egwEvent['end'] - $event['end']) < 120) + { + $retval = $egwEvent['id']; + if ($egwEvent['recur_type'] != MCAL_RECUR_NONE && + $event['recur_type'] == MCAL_RECUR_NONE && $event['recurrence'] != 0) + { + $retval .= ':' . (int)$event['recurrence']; + } + $matchingEvents[] = $retval; + return $matchingEvents; + } + } + if ($filter == 'exact') return array(); } unset($event['id']); - if($foundEvents = parent::search(array( - 'query' => $query, - ))) + if (isset($event['whole_day']) && $event['whole_day']) { - if(is_array($foundEvents)) + if ($filter == 'relax') { - $event = array_shift($foundEvents); - return $event['id']; + $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); + } + elseif (isset($event['start'])) + { + if ($filter != 'master') + { + 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)); + } } } - return false; + + if ($filter == 'master') + { + $query[] = 'recur_type!='. MCAL_RECUR_NONE; + } + + // 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)) + { + 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']; + } + } + } + } + } + if (empty($event['uid'])) + { + $matchFields = array('title', 'priority', 'public', 'non_blocking'); + switch ($filter) + { + case 'relax': + $matchFields[] = 'location'; + case 'master': + break; + default: + $matchFields[] = 'description'; + $matchFields[] = 'location'; + } + + foreach ($matchFields as $key) + { + if (!empty($event[$key])) $query['cal_'.$key] = $event[$key]; + } + } + else + { + $query['cal_uid'] = $event['uid']; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['uid'] . ')[EventUID]'); + } + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[QUERY]: ' . array2string($query)); + } + if (!count($users) || !($foundEvents = + $this->so->search(null, null, $users, 0, 'owner', $query))) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[NO MATCH]'); + } + return $matchingEvents; + } + + $pseudos = array(); + + foreach($foundEvents as $egwEvent) + { + if (in_array($egwEvent['id'], $matchingEvents)) continue; + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[FOUND]: ' . array2string($egwEvent)); + } + + // convert timezone id of event to tzid (iCal id like 'Europe/Berlin') + if (!$egwEvent['tz_id'] || !($egwEvent['tzid'] = calendar_timezones::id2tz($egwEvent['tz_id']))) + { + $egwEvent['tzid'] = egw_time::$server_timezone->getName(); + } + + + // check times + if ($filter != 'relax') + { + if (isset($event['whole_day'])&& $event['whole_day']) + { + if (!$this->so->isWholeDay($egwEvent)) + { + 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; + } + } + } + + if ($filter != 'master') + { + // check categories + if (!is_array($event['category'])) $event['category'] = array(); + $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; + } + } + /* + // check information + $matchFields = array(); + foreach ($matchFields 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; // next foundEvent + } + } + */ + + 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 + $pseudos[] = $egwEvent['id'] . ':' . $event['start']; + continue; + } + } + elseif ($filter != 'relax') + { + // check exceptions + // $exceptions[$remote_ts] = $egw_ts + $exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['$tzid'], 0, 0, 'map'); + // remove leading exceptions + foreach ($exceptions as $key => $day) + { + if ($day <= $event['start']) unset($exceptions['key']); + } + if (is_array($event['recur_excpetion'])) + { + foreach ($event['recur_excpetion'] 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_excpetion'])); + } + continue; + } + } + + // check recurrence information + foreach (array('recur_type', 'recur_interval') 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 + } + // append pseudos as last entries + $matchingEvents = array_merge($matchingEvents, $pseudos); + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[MATCHES]:' . array2string($matchingEvents)); + } + return $matchingEvents; } /** @@ -1528,17 +1854,19 @@ class calendar_boupdate extends calendar_bo * SINGLE a single event * SERIES-MASTER the series master * SERIES-EXCEPTION event is a real exception - * SERIES-EXCEPTION-STATUS event is a status only exception + * SERIES-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-EXCEPTION-STATUS or SERIES-EXCEPTION-PROPAGATE + * 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 - $return_master = false; //default + $master_event = false; //default + $stored_event = false; + $recurrence_event = false; if ($event['recur_type'] != MCAL_RECUR_NONE) { @@ -1547,68 +1875,126 @@ class calendar_boupdate extends calendar_bo else { // SINGLE, SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS - if (empty($event['uid']) && $event['id'] > 0 && ($stored_event = $this->read($event['id']))) + if (($foundEvents = $this->find_event($event, 'exact'))) { - $event['uid'] = $stored_event['uid']; // restore the UID if it was not delivered - } - - if (isset($event['uid']) - && $event['recurrence'] - && ($master_event = $this->read($event['uid'])) - && isset($master_event['recur_type']) - && $master_event['recur_type'] != MCAL_RECUR_NONE) - { - // SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS - $return_master = true; // we have a valid master and can return it - - if (isset($event['id']) && $master_event['id'] != $event['id']) + // We found the exact match + $eventID = array_shift($foundEvents); + if ($this->log) { - $type = 'SERIES-EXCEPTION'; // this is an existing exception + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "()[EVENT]: $eventID"); + } + if (strstr($eventID, ':')) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; + 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 { - $type = 'SERIES-EXCEPTION-STATUS'; // default if we cannot find a proof for a fundamental change - // the recurrence_event is the master event with start and end adjusted to the recurrence - $recurrence_event = $master_event; - $recurrence_event['start'] = $event['recurrence']; - $recurrence_event['end'] = $event['recurrence'] + ($master_event['end'] - $master_event['start']); - // check for changed data - foreach (array('start','end','uid','title','location', - 'priority','public','special','non_blocking') as $key) - { - if (!empty($event[$key]) && $recurrence_event[$key] != $event[$key]) - { - if (isset($event['id'])) - { - $type = 'SERIES-EXCEPTION-PROPAGATE'; - } - else - { - $type = 'SERIES-EXCEPTION'; // this is a new exception - } - break; - } - } - // the event id here is always the id of the master event - // unset it to prevent confusion of stored event and master event - unset($event['id']); + $stored_event = $this->read($eventID, 0, false, 'server'); + } + if (empty($event['uid'])) + { + $event['uid'] = $stored_event['uid']; // restore the UID if it was not delivered } } - else + if ($type == 'SINGLE' && + ($foundEvents = $this->find_event($event, 'master'))) { - // SINGLE - $type = 'SINGLE'; + // Let's try to find a related series + foreach ($foundEvents as $eventID) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "()[MASTER]: $eventID"); + } + if (($master_event = $this->read($eventID, 0, false, 'server'))) + { + if (isset($stored_event['id']) && $master_event['id'] != $stored_event['id']) + { + $type = 'SERIES-EXCEPTION'; // this is an existing exception + } + elseif (in_array($event['start'], $master_event['recur_exception'])) + { + $type='SERIES-PSEUDO-EXCEPTION'; // new pseudo exception? + $recurrence_event = $event; + break; + } + elseif (isset($event['recurrence']) && + in_array($event['recurrence'], $master_event['recur_exception'])) + { + $type = 'SERIES-EXCEPTION'; + break; + } + else + { + // try to find a suitable pseudo exception date + $egw_rrule = calendar_rrule::event2rrule($master_event, false); + $egw_rrule->rewind(); + while ($egw_rrule->valid()) + { + $occurrence = egw_time::to($egw_rrule->current(), 'server'); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() try occurrence ' . $egw_rrule->current() . " ($occurrence)"); + } + if ($event['start'] == $occurrence) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; // let's try a pseudo exception + $recurrence_event = $event; + break 2; + } + if (isset($event['recurrence']) && $event['recurrence'] == $occurrence) + { + $type = 'SERIES-EXCEPTION-PROPAGATE'; + if ($stored_event) + { + unset($stored_event['id']); // signal the true exception + $stored_event['recur_type'] = MCAL_RECUR_NONE; + } + break 2; + } + $egw_rrule->next_no_exception(); + } + } + } + } } - } - // read existing event - if (isset($event['id'])) - { - $stored_event = $this->read($event['id']); + // 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]) + { + $type = 'SERIES-EXCEPTION-PROPAGATE'; + 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 ($return_master) + if (is_array($master_event)) { $acl_edit = $this->check_perms(EGW_ACL_EDIT, $master_event['id']); } @@ -1627,8 +2013,40 @@ class calendar_boupdate extends calendar_bo return array( 'type' => $type, 'acl_edit' => $acl_edit, - 'stored_event' => is_array($stored_event) ? $stored_event : false, - 'master_event' => $return_master ? $master_event : false, + 'stored_event' => $stored_event, + 'master_event' => $master_event, ); } + + /* + * Translates all timestamps for a given event from servert-ime to user-time. + * The update() and save() methods expect timestamps in user-time. + * @param &$event the event we are working on + * + */ + function server2usertime (&$event) + { + // we run all dates through date2usertime, to adjust to user-time + foreach(array('start','end','recur_enddate','recurrence') as $ts) + { + // we convert here from server-time to timestamps in user-time! + if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2usertime($event[$ts]) : 0; + } + // same with the recur exceptions + if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + { + foreach($event['recur_exception'] as $n => $date) + { + $event['recur_exception'][$n] = $this->date2usertime($date); + } + } + // same with the alarms + if (isset($event['alarm']) && is_array($event['alarm'])) + { + foreach($event['alarm'] as $id => $alarm) + { + $event['alarm'][$id]['time'] = $this->date2usertime($alarm['time']); + } + } + } } \ No newline at end of file diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index a7d692e268..2a6ca28545 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -179,7 +179,7 @@ class calendar_ical extends calendar_boupdate * @param string $version='1.0' could be '2.0' too * @param string $method='PUBLISH' * @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) + * default 0 => export whole series (or events, if not recurring) * @return string/boolean string with iCal or false on error (eg. no permission to read the event) */ function &exportVCal($events, $version='1.0', $method='PUBLISH', $recur_date=0) @@ -219,26 +219,97 @@ class calendar_ical extends calendar_boupdate if (!is_array($events)) $events = array($events); $vtimezones_added = array(); - foreach($events as $event) + foreach ($events as $event) { $mailtoOrganizer = false; $organizerCN = false; + $recurrence = $recur_date; + $tzid = null; - if (strpos($this->productName, 'palmos') !== false) + if (!is_array($event) + && !($event = $this->read($event, $recurrence, false, 'server'))) { - $date_format = 'ts'; + if ($this->read($event, $recurrence, true, 'server')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() User does not have the permission to read event $_id.\n", + 3,$this->logfile); + } + return -1; // Permission denied + } + else + { + $retval = false; // Entry does not exist + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Event $_id not found.\n", + 3,$this->logfile); + } + } + continue; + } + + if ($this->tzid) + { + // explicit device timezone + $tzid = $this->tzid; } else { - $date_format = 'server'; + // use event's timezone + $tzid = $event['tzid']; } - if (!is_array($event) - && !($event = $this->read($event, $recur_date, false, $date_format))) + + if (!isset(self::$tz_cache[$event['tzid']])) { - return false; // no permission to read $cal_id + self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); } - if ($recur_date) + + if ($this->so->isWholeDay($event)) $event['whole_day'] = true; + + if ($this->log) { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['id']. ',' . $recurrence . ")\n" . + array2string($event)."\n",3,$this->logfile); + } + + if ($recurrence) + { + if (!($master = $this->read($event['id'], 0, true, 'server'))) continue; + + if (!isset($this->supportedFields['participants'])) + { + $days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'tz_rrule'); + if (isset($days[$recurrence])) + { + $recurrence = $days[$recurrence]; // use remote representation + } + else + { + // We don't need status only exceptions + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($_id, $recurrence) Gratuitous pseudo exception, skipped ...\n", + 3,$this->logfile); + } + continue; // unsupported status only exception + } + } + else + { + $days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'rrule'); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($days)."\n",3,$this->logfile); + } + $recurrence = $days[$recurrence]; // use remote representation + } // force single event foreach (array('recur_enddate','recur_interval','recur_exception','recur_data','recur_date','id','etag') as $name) { @@ -249,34 +320,12 @@ class calendar_ical extends calendar_boupdate elseif ($event['recur_enddate']) { $time = new egw_time($event['recur_enddate'],egw_time::$server_timezone); - if (!isset(self::$tz_cache[$event['tzid']])) - { - self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); - } // all calculations in the event's timezone $time->setTimezone(self::$tz_cache[$event['tzid']]); $time->setTime(23, 59, 59); - $event['recur_enddate'] = $this->date2ts($time); + $event['recur_enddate'] = egw_time::to($time, 'server'); } - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . - array2string($event)."\n",3,$this->logfile); - } - - if ($this->tzid === false) - { - $tzid = null; - } - elseif ($this->tzid) - { - $tzid = $this->tzid; - } - else - { - $tzid = $event['tzid']; - } // check if tzid of event (not only recuring ones) is already added to export if ($tzid && $tzid != 'UTC' && !in_array($tzid,$vtimezones_added)) { @@ -332,6 +381,49 @@ class calendar_ical extends calendar_boupdate } } + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + $exceptions = array(); + + // dont use "virtual" exceptions created by participant status for GroupDAV or file export + if (!in_array($this->productManufacturer,array('file','groupdav'))) + { + $filter = isset($this->supportedFields['participants']) ? 'rrule' : 'tz_rrule'; + $exceptions = $this->so->get_recurrence_exceptions($event, $tzid, 0, 0, $filter); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS)\n" . + array2string($exceptions)."\n",3,$this->logfile); + } + } + elseif (is_array($event['recur_exception'])) + { + $exceptions = array_unique($event['recur_exception']); + sort($exceptions); + } + $event['recur_exception'] = $exceptions; + // Adjust the event start -- must not be an exception + $length = $event['end'] - $event['start']; + $rriter = calendar_rrule::event2rrule($event, false, $tzid); + $rriter->rewind(); + if ($rriter->valid()) + { + $event['start'] = egw_time::to($rriter->current, 'server'); + $event['end'] = $event['start'] + $length; + foreach($exceptions as $key => $day) + { + // remove leading exceptions + if ($day <= $event['start']) unset($exceptions[$key]); + } + $event['recur_exception'] = $exceptions; + } + else + { + // the series dissolved completely into exceptions + continue; + } + } + foreach ($egwSupportedFields as $icalFieldName => $egwFieldName) { if (!isset($this->supportedFields[$egwFieldName])) continue; @@ -419,17 +511,22 @@ class calendar_ical extends calendar_boupdate break; case 'DTSTART': - $attributes['DTSTART'] = self::getDateTime($event['start'],$tzid,$parameters['DTSTART']); + if (!isset($event['whole_day'])) + { + $attributes['DTSTART'] = self::getDateTime($event['start'],$tzid,$parameters['DTSTART']); + } break; case 'DTEND': // write start + end of whole day events as dates - if ($this->isWholeDay($event)) + if (isset($event['whole_day'])) { $event['end-nextday'] = $event['end'] + 12*3600; // we need the date of the next day, as DTEND is non-inclusive (= exclusive) in rfc2445 foreach (array('start' => 'DTSTART','end-nextday' => 'DTEND') as $f => $t) { - $arr = $this->date2array($event[$f]); + $time = new egw_time($event[$f],egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $arr = egw_time::to($time,'array'); $vevent->setAttribute($t, array('year' => $arr['year'],'month' => $arr['month'],'mday' => $arr['day']), array('VALUE' => 'DATE')); } @@ -443,7 +540,7 @@ class calendar_ical extends calendar_boupdate case 'RRULE': if ($event['recur_type'] == MCAL_RECUR_NONE) break; // no recuring event - $rriter = calendar_rrule::event2rrule($event,$date_format != 'server'); + $rriter = calendar_rrule::event2rrule($event, false, $tzid); $rrule = $rriter->generate_rrule($version); if ($version == '1.0') { @@ -458,45 +555,35 @@ class calendar_ical extends calendar_boupdate case 'EXDATE': if ($event['recur_type'] == MCAL_RECUR_NONE) break; - $days = array(); - // dont use "virtual" exceptions created by participant status for GroupDAV or file export - if (!in_array($this->productManufacturer,array('file','groupdav'))) + if (!empty($event['recur_exception'])) { - $tz_id = ($tzid != $event['tzid'] ? $tzid : null); - $days = $this->so->get_recurrence_exceptions($event, $tz_id); - } - if (is_array($event['recur_exception'])) - { - $days = array_merge($days,$event['recur_exception']); // can NOT use +, as it overwrites numeric indexes - } - if (!empty($days)) - { - $days = array_unique($days); - sort($days); // use 'DATE' instead of 'DATE-TIME' on whole day events - if ($this->isWholeDay($event)) + if (isset($event['whole_day'])) { $value_type = 'DATE'; - foreach ($days as $id => $timestamp) + foreach ($event['recur_exception'] as $id => $timestamp) { - $arr = $this->date2array($timestamp); + $time = new egw_time($timestamp,egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $arr = egw_time::to($time,'array'); $days[$id] = array( 'year' => $arr['year'], 'month' => $arr['month'], 'mday' => $arr['day'], ); } + $event['recur_exception'] = $days; } else { $value_type = 'DATE-TIME'; - foreach ($days as &$timestamp) + foreach ($event['recur_exception'] as $key => $timestamp) { - $timestamp = self::getDateTime($timestamp,$tzid,$parameters['EXDATE']); + $event['recur_exception'][$key] = self::getDateTime($timestamp,$tzid,$parameters['EXDATE']); } } $attributes['EXDATE'] = ''; - $values['EXDATE'] = $days; + $values['EXDATE'] = $event['recur_exception']; $parameters['EXDATE']['VALUE'] = $value_type; } break; @@ -544,12 +631,14 @@ class calendar_ical extends calendar_boupdate { $icalFieldName = 'X-RECURRENCE-ID'; } - if ($recur_date) + if ($recurrence) { - // We handle a status only exception - if ($this->isWholeDay($event)) + // We handle a pseudo exception + if (isset($event['whole_day'])) { - $arr = $this->date2array($recur_date); + $time = new egw_time($recurrence,egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $arr = egw_time::to($time,'array'); $vevent->setAttribute($icalFieldName, array( 'year' => $arr['year'], 'month' => $arr['month'], @@ -559,7 +648,7 @@ class calendar_ical extends calendar_boupdate } else { - $attributes[$icalFieldName] = self::getDateTime($recur_date,$tzid,$parameters[$icalFieldName]); + $attributes[$icalFieldName] = self::getDateTime($recurrence,$tzid,$parameters[$icalFieldName]); } } elseif ($event['recurrence'] && $event['reference']) @@ -567,9 +656,11 @@ class calendar_ical extends calendar_boupdate // $event['reference'] is a calendar_id, not a timestamp if (!($revent = $this->read($event['reference']))) break; // referenced event does not exist - if ($this->isWholeDay($revent)) + if (isset($revent['whole_day'])) { - $arr = $this->date2array($event['recurrence']); + $time = new egw_time($event['recurrence'],egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $arr = egw_time::to($time,'array'); $vevent->setAttribute($icalFieldName, array( 'year' => $arr['year'], 'month' => $arr['month'], @@ -669,14 +760,34 @@ class calendar_ical extends calendar_boupdate $attributes['DTSTAMP'] = time(); foreach ($event['alarm'] as $alarmID => $alarmData) { + // skip over alarms that don't have the minimum required info + if (!$alarmData['offset'] && !$alarmData['time']) continue; + // skip alarms not being set for all users and alarms owned by other users if ($alarmData['all'] != true && $alarmData['owner'] != $this->user) { continue; } + if ($alarmData['offset']) + { + $alarmData['time'] = $event['start'] - $alarmData['offset']; + } + + $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $event['description'])); + if ($version == '1.0') { + if ($event['title']) $description = $event['title']; + if ($description) + { + $values['DALARM']['snooze_time'] = ''; + $values['DALARM']['repeat count'] = ''; + $values['DALARM']['display text'] = $description; + $values['AALARM']['snooze_time'] = ''; + $values['AALARM']['repeat count'] = ''; + $values['AALARM']['display text'] = $description; + } $attributes['DALARM'] = self::getDateTime($alarmData['time'],$tzid,$parameters['DALARM']); $attributes['AALARM'] = self::getDateTime($alarmData['time'],$tzid,$parameters['AALARM']); // lets take only the first alarm @@ -686,15 +797,10 @@ class calendar_ical extends calendar_boupdate { // VCalendar 2.0 / RFC 2445 - $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $event['description'])); - - // skip over alarms that don't have the minimum required info - if (!$alarmData['offset'] && !$alarmData['time']) continue; - // RFC requires DESCRIPTION for DISPLAY if (!$event['title'] && !$description) continue; - if ($this->isWholeDay($event) && $alarmData['offset']) + if (isset($event['whole_day']) && $alarmData['offset']) { $alarmData['time'] = $event['start'] - $alarmData['offset']; $alarmData['offset'] = false; @@ -721,7 +827,7 @@ class calendar_ical extends calendar_boupdate foreach ($attributes as $key => $value) { - foreach (is_array($value)&&$parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData) + foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData) { $valueData = $GLOBALS['egw']->translation->convert($valueData,$GLOBALS['egw']->translation->charset(),'UTF-8'); $paramData = (array) $GLOBALS['egw']->translation->convert(is_array($value) ? @@ -729,8 +835,9 @@ class calendar_ical extends calendar_boupdate $GLOBALS['egw']->translation->charset(),'UTF-8'); $valuesData = (array) $GLOBALS['egw']->translation->convert($values[$key], $GLOBALS['egw']->translation->charset(),'UTF-8'); + $content = $valueData . implode(';', $valuesData); - if (preg_match('/[^\x20-\x7F]/', $valueData) || + if (preg_match('/[^\x20-\x7F]/', $content) || ($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))) { $paramData['CHARSET'] = 'UTF-8'; @@ -826,12 +933,6 @@ class calendar_ical extends calendar_boupdate */ function importVCal($_vcalData, $cal_id=-1, $etag=null, $merge=false, $recur_date=0) { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . - array2string($_vcalData)."\n",3,$this->logfile); - } - if (!is_array($this->supportedFields)) $this->setSupportedFields(); if (!($events = $this->icaltoegw($_vcalData,$cal_id,$etag,$recur_date))) @@ -852,19 +953,187 @@ class calendar_ical extends calendar_boupdate } foreach ($events as $event) { + if ($this->so->isWholeDay($event)) $event['whole_day'] = true; + if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($event)."\n",3,$this->logfile); } + + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + // Adjust the event start -- no exceptions before and at the start + $length = $event['end'] - $event['start']; + $rriter = calendar_rrule::event2rrule($event, false); + $rriter->rewind(); + if (!$rriter->valid()) continue; // completely disolved into exceptions + + $newstart = egw_time::to($rriter->current, 'server'); + if ($newstart != $event['start']) + { + // leading exceptions skiped + $event['start'] = $newstart; + $event['end'] = $newstart + $length; + } + + $exceptions = $event['recur_exception']; + foreach($exceptions as $key => $day) + { + // remove leading exceptions + if ($day <= $event['start']) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(): event SERIES-MASTER skip leading exception ' . + $day . "\n",3,$this->logfile); + } + unset($exceptions[$key]); + } + } + $event['recur_exception'] = $exceptions; + } + $updated_id = false; $event_info = $this->get_event_info($event); - // common adjustments for new events - if (!is_array($event_info['stored_event'])) + // common adjustments for existing events + if (is_array($event_info['stored_event'])) { + if (empty($event['uid'])) + { + $event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered + } + if ($merge) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "()[MERGE]\n",3,$this->logfile); + } + + // overwrite with server data for merge + foreach ($event_info['stored_event'] as $key => $value) + { + switch ($key) + { + case 'participants_types': + continue; + + case 'participants': + foreach ($event_info['stored_event']['participants'] as $uid => $status) + { + // Is a participant and no longer present in the event? + if (!isset($event['participants'][$uid])) + { + // Add it back in + $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; + } + } + break; + + default: + if (!empty($value)) $event[$key] = $value; + } + } + } + else + { + // no merge + if (!isset($this->supportedFields['participants']) || !count($event['participants'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() No participants\n",3,$this->logfile); + } + + // If this is an updated meeting, and the client doesn't support + // participants OR the event no longer contains participants, add them back + $event['participants'] = $event_info['stored_event']['participants']; + $event['participant_types'] = $event_info['stored_event']['participant_types']; + } + else + { + // if the client does not return a status, we restore the original one + foreach ($event['participants'] as $uid => $status) + { + // Is it a resource and no longer present in the event? + if ($status[0] == 'X') + { + if (isset($event_info['stored_event']['participants'][$uid])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Restore status for $uid\n",3,$this->logfile); + } + $event['participants']['uid'] = $event_info['stored_event']['participants'][$uid]; + } + else + { + $event['participants']['uid'] = calendar_so::combine_status('U'); + } + } + } + } + + foreach ($event_info['stored_event']['participants'] as $uid => $status) + { + // Is it a resource and no longer present in the event? + if ($uid[0] == 'r' && !isset($event['participants'][$uid])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Restore resource $uid to status $status\n",3,$this->logfile); + } + // Add it back in + $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; + } + } + + if ($event['whole_day'] && $event['tzid'] != $event_info['stored_event']['tzid']) + { + // Adjust dates to original TZ + $time = new egw_time($event['start'],egw_time::$server_timezone); + $time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']); + $event['start'] = egw_time::to($time,'server'); + $time = new egw_time($event['end'],egw_time::$server_timezone); + $time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']); + $time->setTime(23, 59, 59); + $event['end'] = egw_time::to($time,'server'); + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + foreach ($event['recur_exception'] as $key => $day) + { + $time = new egw_time($day,egw_time::$server_timezone); + $time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']); + $event['recur_exception'][$key] = egw_time::to($time,'server'); + } + } + elseif($event['recurrence']) + { + $time = new egw_time($event['recurrence'],egw_time::$server_timezone); + $time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']); + $event['recurrence'] = egw_time::to($time,'server'); + } + } + + calendar_rrule::rrule2tz($event, $event_info['stored_event']['start'], + $event_info['stored_event']['tzid']); + + $event['tzid'] = $event_info['stored_event']['tzid']; + // avoid that iCal changes the organizer, which is not allowed + $event['owner'] = $event_info['stored_event']['owner']; + } + } + else // common adjustments for new events + { + unset($event['id']); // set non blocking all day depending on the user setting - if ($this->isWholeDay($event) && $this->nonBlockingAllday) + if (isset($event['whole_day']) && $this->nonBlockingAllday) { $event['non_blocking'] = 1; } @@ -883,68 +1152,23 @@ class calendar_ical extends calendar_boupdate $status = calendar_so::combine_status($status, 1, 'CHAIR'); $event['participants'] = array($event['owner'] => $status); } - } - - // common adjustments for existing events - if (is_array($event_info['stored_event'])) - { - if (empty($event['uid'])) - { - $event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered - } - if ($merge) - { - // overwrite with server data for merge - foreach ($event_info['stored_event'] as $key => $value) - { - switch ($key) - { - case 'participants_types': - continue; - - case 'participants': - foreach ($event_info['stored_event']['participants'] as $uid => $status) - { - // Is a participant and no longer present in the event? - if (!isset($event['participants'][$uid])) - { - // Add it back in - $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; - } - } - break; - - default: - if (!empty($value)) - { - $event[$key] = $value; - } - } - } - } else { - // no merge - if (!isset($this->supportedFields['participants']) || !count($event['participants'])) - { - // If this is an updated meeting, and the client doesn't support - // participants OR the event no longer contains participants, add them back - $event['participants'] = $event_info['stored_event']['participants']; - $event['participant_types'] = $event_info['stored_event']['participant_types']; - } - - foreach ($event_info['stored_event']['participants'] as $uid => $status) + foreach ($event['participants'] as $uid => $status) { // Is it a resource and no longer present in the event? - if ( $uid[0] == 'r' && !isset($event['participants'][$uid]) ) + if ($status[0] == 'X') { - // Add it back in - $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; + if ($uid == $event['owner']) + { + $event['participants']['uid'] = calendar_so::combine_status('A', 1, 'CHAIR'); + } + else + { + $event['participants']['uid'] = calendar_so::combine_status('U'); + } } } - $event['tzid'] = $event_info['stored_event']['tzid']; - // avoid that iCal changes the organizer, which is not allowed - $event['owner'] = $event_info['stored_event']['owner']; } } @@ -990,7 +1214,7 @@ class calendar_ical extends calendar_boupdate } break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': // nothing to do here break; } @@ -1017,8 +1241,6 @@ class calendar_ical extends calendar_boupdate error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): event SINGLE\n",3,$this->logfile); } - //Horde::logMessage('importVCAL event SINGLE', - // __FILE__, __LINE__, PEAR_LOG_DEBUG); // update the event if ($event_info['acl_edit']) @@ -1027,6 +1249,7 @@ class calendar_ical extends calendar_boupdate unset($event['recurrence']); $event['reference'] = 0; $event_to_store = $event; // prevent $event from being changed by the update method + $this->server2usertime($event_to_store); $updated_id = $this->update($event_to_store, true); unset($event_to_store); } @@ -1038,27 +1261,81 @@ class calendar_ical extends calendar_boupdate error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): event SERIES-MASTER\n",3,$this->logfile); } - //Horde::logMessage('importVCAL event SERIES-MASTER', - // __FILE__, __LINE__, PEAR_LOG_DEBUG); - // remove all known "status only" exceptions and update the event + // remove all known pseudo exceptions and update the event if ($event_info['acl_edit']) { - $days = $this->so->get_recurrence_exceptions($event); + $filter = isset($this->supportedFields['participants']) ? 'map' : 'tz_map'; + $days = $this->so->get_recurrence_exceptions($event_info['stored_event'], $this->tzid, 0, 0, $filter); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS MAPPING):\n" . + array2string($days)."\n",3,$this->logfile); + } if (is_array($days)) { $recur_exceptions = array(); + + if (!isset($days[$event_info['stored_event']['start']]) && + $event_info['stored_event']['start'] < $event['start']) + { + // We started with a pseudo exception and moved the + // event start therefore to the future; let's try to go back + $exceptions = $this->so->get_recurrence_exceptions($event_info['stored_event'], $this->tzid, 0, 0, 'rrule'); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(START ADJUSTMENT):\n" . + array2string($exceptions)."\n",3,$this->logfile); + } + $startdate = $event_info['stored_event']['start']; + $length = $event['end'] - $event['start']; + $rriter = calendar_rrule::event2rrule($event_info['stored_event'], false); + $rriter->rewind(); + do + { + // start is a pseudo excpetion for sure + $rriter->next_no_exception(); + $day = $this->date2ts($rriter->current()); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(): event SERIES-MASTER try leading pseudo exception ' . + $day . "\n",3,$this->logfile); + } + if ($day >= $event['start']) break; + if (!isset($exceptions[$day])) + { + // all leading occurrences have to be exceptions; + // if not -> no restore + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(): event SERIES-MASTER pseudo exception series broken at ' . + $rriter->current() . "!\n",3,$this->logfile); + } + $startdate = $event['start']; + $recur_exceptions = array(); + break; + } + $recur_exceptions[] = $day; + } while ($rriter->valid()); + + $event['start'] = $startdate; + $event['end'] = $startdate + $length; + } + foreach ($event['recur_exception'] as $recur_exception) { - if (!in_array($recur_exception, $days)) + if (isset($days[$recur_exception])) { - $recur_exceptions[] = $recur_exception; + $recur_exceptions[] = $days[$recur_exception]; } } $event['recur_exception'] = $recur_exceptions; } $event_to_store = $event; // prevent $event from being changed by the update method + $this->server2usertime($event_to_store); $updated_id = $this->update($event_to_store, true); unset($event_to_store); } @@ -1071,8 +1348,6 @@ class calendar_ical extends calendar_boupdate error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): event SERIES-EXCEPTION\n",3,$this->logfile); } - //Horde::logMessage('importVCAL event SERIES-EXCEPTION', - // __FILE__, __LINE__, PEAR_LOG_DEBUG); // update event if ($event_info['acl_edit']) @@ -1087,28 +1362,56 @@ class calendar_ical extends calendar_boupdate { // We create a new exception unset($event['id']); - $event_info['master_event']['recur_exception'] = array_unique(array_merge($event_info['master_event']['recur_exception'], array($event['recurrence']))); - $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method - $this->update($event_to_store, true); - unset($event_to_store); + 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['recurrence']))); + + // Adjust the event start -- must not be an exception + $length = $event_info['master_event']['end'] - $event_info['master_event']['start']; + $rriter = calendar_rrule::event2rrule($event_info['master_event'], false); + $rriter->rewind(); + if ($rriter->valid()) + { + $newstart = egw_time::to($rriter->current, 'server'); + foreach($event_info['master_event']['recur_exception'] as $key => $day) + { + // remove leading exceptions + if ($day < $newstart) + { + unset($event_info['master_event']['recur_exception'][$key]); + } + } + } + if ($event_info['master_event']['start'] < $newstart) + { + $event_info['master_event']['start'] = $newstart; + $event_info['master_event']['end'] = $newstart + $length; + $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method + $this->server2usertime($event_to_store); + $this->update($event_to_store, true); + unset($event_to_store); + } $event['reference'] = $event_info['master_event']['id']; $event['category'] = $event_info['master_event']['category']; $event['owner'] = $event_info['master_event']['owner']; } $event_to_store = $event; // prevent $event from being changed by update method - $updated_id = $this->update($event_to_store, true, true, false, false); + $this->server2usertime($event_to_store); + $updated_id = $this->update($event_to_store, true); unset($event_to_store); } break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. - "(): event SERIES-EXCEPTION-STATUS\n",3,$this->logfile); + "(): event SERIES-PSEUDO-EXCEPTION\n",3,$this->logfile); } - //Horde::logMessage('importVCAL event SERIES-EXCEPTION-STATUS', + //Horde::logMessage('importVCAL event SERIES-PSEUDO-EXCEPTION', // __FILE__, __LINE__, PEAR_LOG_DEBUG); if ($event_info['acl_edit']) @@ -1126,6 +1429,7 @@ class calendar_ical extends calendar_boupdate // save the series master with the adjusted exceptions $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method + $this->server2usertime($event_to_store); $updated_id = $this->update($event_to_store, true, true, false, false); unset($event_to_store); } @@ -1136,7 +1440,7 @@ class calendar_ical extends calendar_boupdate // read stored event into info array for fresh stored (new) events if (!is_array($event_info['stored_event']) && $updated_id > 0) { - $event_info['stored_event'] = $this->read($updated_id); + $event_info['stored_event'] = $this->read($updated_id, 0, false, 'server'); } // update status depending on the given event type @@ -1162,19 +1466,20 @@ class calendar_ical extends calendar_boupdate } break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': if (is_array($event_info['master_event'])) // status update requires a stored master event { + $recurrence = $this->date2usertime($event['recurrence']); if ($event_info['acl_edit']) { // update all participants if we have the right to do that - $this->update_status($event, $event_info['master_event'], $event['recurrence']); + $this->update_status($event, $event_info['stored_event'], $recurrence); } elseif (isset($event['participants'][$this->user]) || isset($event_info['master_event']['participants'][$this->user])) { // update the users status only $this->set_status($event_info['master_event']['id'], $this->user, - ($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $event['recurrence'], true); + ($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $recurrence, true); } } break; @@ -1189,7 +1494,7 @@ class calendar_ical extends calendar_boupdate $return_id = is_array($event_info['stored_event']) ? $event_info['stored_event']['id'] : false; break; - case 'SERIES-EXCEPTION-STATUS': + case 'SERIES-PSEUDO-EXCEPTION': $return_id = is_array($event_info['master_event']) ? $event_info['master_event']['id'] . ':' . $event['recurrence'] : false; break; @@ -1202,7 +1507,7 @@ class calendar_ical extends calendar_boupdate else { // we did not have sufficient rights to propagate the status only exception to a real one - // we have to keep the SERIES-EXCEPTION-STATUS id and keep the event untouched + // we have to keep the SERIES-PSEUDO-EXCEPTION id and keep the event untouched $return_id = $event_info['master_event']['id'] . ':' . $event['recurrence']; } break; @@ -1210,12 +1515,12 @@ class calendar_ical extends calendar_boupdate if ($this->log) { - $event_info['stored_event'] = $this->read($event_info['stored_event']['id']); + $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; } @@ -1305,6 +1610,13 @@ class calendar_ical extends calendar_boupdate if (isset($deviceInfo) && is_array($deviceInfo)) { + /* + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() ' . array2string($deviceInfo) . "\n",3,$this->logfile); + } + */ if (isset($deviceInfo['uidExtension']) && $deviceInfo['uidExtension']) { @@ -1541,6 +1853,12 @@ class calendar_ical extends calendar_boupdate function icaltoegw($_vcalData, $cal_id=-1, $etag=null, $recur_date=0) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_vcalData)."\n",3,$this->logfile); + } + $events = array(); if ($this->tzid) @@ -1706,10 +2024,11 @@ class calendar_ical extends calendar_boupdate ); break; case 'CLASS': - $vcardData['public'] = (int)(strtolower($attributes['value']) == 'public'); + $vcardData['public'] = (int)(strtolower($attributes['value']) == 'public'); break; case 'DESCRIPTION': - $vcardData['description'] = $attributes['value']; + $vcardData['description'] = str_replace("\r\n", "\n", $attributes['value']); + $vcardData['description'] = str_replace("\r", "\n", $vcardData['description']); if (preg_match('/\s*\[UID:(.+)?\]/Usm', $attributes['value'], $matches)) { if (!isset($vCardData['uid']) @@ -1724,7 +2043,8 @@ class calendar_ical extends calendar_boupdate $vcardData['recurrence'] = $attributes['value']; break; case 'LOCATION': - $vcardData['location'] = $attributes['value']; + $vcardData['location'] = str_replace("\r\n", "\n", $attributes['value']); + $vcardData['location'] = str_replace("\r", "\n", $vcardData['location']); break; case 'RRULE': $recurence = $attributes['value']; @@ -1749,34 +2069,27 @@ class calendar_ical extends calendar_boupdate case 'W': case 'WEEKLY': $days = array(); - if (preg_match('/W(\d+)((?i: [AEFHMORSTUW]*)+)?( +([^ ]*))$/',$recurence, $recurenceMatches)) // 1.0 + if (preg_match('/W(\d+) *((?i: [AEFHMORSTUW]{2})+)?( +([^ ]*))$/',$recurence, $recurenceMatches)) // 1.0 { $vcardData['recur_interval'] = $recurenceMatches[1]; if (empty($recurenceMatches[2])) { - if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches)) - { - if ($recurenceMatches[1]) $vcardData['recur_count'] = $recurenceMatches[1]; - } - else - { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); - } $days[0] = strtoupper(substr(date('D', $vcardData['start']),0,2)); } else { $days = explode(' ',trim($recurenceMatches[2])); - - if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches)) - { - if ($recurenceMatches[1]) $vcardData['recur_count'] = $recurenceMatches[1]; - } - else - { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); - } } + + if (preg_match('/#(\d+)/',$recurenceMatches[4],$repeatMatches)) + { + if ($repeatMatches[1]) $vcardData['recur_count'] = $repeatMatches[1]; + } + else + { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); + } + $recur_days = $this->recur_days_1_0; } elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0 @@ -1832,7 +2145,7 @@ class calendar_ical extends calendar_boupdate elseif (preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches)) { $vcardData['recur_interval'] = $recurenceMatches[1]; - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2])); } else break; @@ -1876,7 +2189,7 @@ class calendar_ical extends calendar_boupdate { $vcardData['recur_interval'] = $recurenceMatches[1]; } - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2])); } elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches)) { @@ -1897,7 +2210,7 @@ class calendar_ical extends calendar_boupdate } else { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[4])); } } break; @@ -1938,7 +2251,7 @@ class calendar_ical extends calendar_boupdate $vcardData['recur_interval'] = $recurenceMatches[1]; if ($recurenceMatches[2] != '#0') { - $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2])); } } else break; @@ -1987,7 +2300,8 @@ class calendar_ical extends calendar_boupdate } break; case 'SUMMARY': - $vcardData['title'] = $attributes['value']; + $vcardData['title'] = str_replace("\r\n", "\n", $attributes['value']); + $vcardData['title'] = str_replace("\r", "\n", $vcardData['title']); break; case 'UID': if (strlen($attributes['value']) >= $minimum_uid_length) @@ -2020,14 +2334,7 @@ class calendar_ical extends calendar_boupdate case 'CATEGORIES': if ($attributes['value']) { - if($version == '1.0') - { - $vcardData['category'] = $this->find_or_add_categories(explode(';',$attributes['value']), $cal_id); - } - else - { - $vcardData['category'] = $this->find_or_add_categories(explode(',',$attributes['value']), $cal_id); - } + $vcardData['category'] = $this->find_or_add_categories(explode(',',$attributes['value']), $cal_id); } else { @@ -2046,7 +2353,7 @@ class calendar_ical extends calendar_boupdate } else { - $status = 'U'; + $status = 'X'; // client did not return the status } $cn = ''; if (preg_match('/MAILTO:([@.a-z0-9_-]+)|MAILTO:"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i', @@ -2101,7 +2408,7 @@ class calendar_ical extends calendar_boupdate { //Horde::logMessage("vevent2egw: group participant $uid", // __FILE__, __LINE__, PEAR_LOG_DEBUG); - if ($status != 'U') + if ($status != 'X' && $status != 'U') { // User tries to reply to the group invitiation $members = $GLOBALS['egw']->accounts->members($uid, true); @@ -2263,10 +2570,11 @@ class calendar_ical extends calendar_boupdate $delta = $event['end'] - $event['start']; $last->modify('+' . $delta . ' seconds'); $last->setTime(0, 0, 0); - $event['recur_enddate'] = $this->date2ts($last); + $event['recur_enddate'] = egw_time::to($last, 'server'); } if ($this->calendarOwner) $event['owner'] = $this->calendarOwner; + if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . @@ -2284,11 +2592,18 @@ class calendar_ical extends calendar_boupdate // this function only supports searching a single event if (count($events) == 1) { + $filter = $relax ? 'relax' : 'exact'; $event = array_shift($events); - return $this->find_event($event, $relax); + if ($this->so->isWholeDay($event)) $event['whole_day'] = true; + return $this->find_event($event, $filter); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."() found:\n" . + array2string($events)."\n",3,$this->logfile); } } - return false; + return array(); } /** diff --git a/calendar/inc/class.calendar_rrule.inc.php b/calendar/inc/class.calendar_rrule.inc.php index 6240406d13..4efab3cced 100644 --- a/calendar/inc/class.calendar_rrule.inc.php +++ b/calendar/inc/class.calendar_rrule.inc.php @@ -5,6 +5,7 @@ * @link http://www.egroupware.org * @package calendar * @author Ralf Becker + * @author Joerg Lehrke * @copyright (c) 2009 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -192,6 +193,12 @@ class calendar_rrule implements Iterator */ protected $lastdayofweek; + /** + * Cached timezone data + * + * @var array id => data + */ + protected static $tz_cache = array(); /** * Constructor @@ -426,6 +433,12 @@ class calendar_rrule implements Iterator public function rewind() { $this->current = clone $this->time; + while ($this->valid() && + $this->exceptions && + in_array($this->current->format('Ymd'),$this->exceptions)) + { + $this->next_no_exception(); + } } /** @@ -517,18 +530,21 @@ class calendar_rrule implements Iterator /** * Generate a VEVENT RRULE * @param string $version='1.0' could be '2.0', too + * + * $return array vCalendar RRULE */ public function generate_rrule($version='1.0') { - static $utc; $repeat_days = array(); $rrule = array(); - if (is_null($utc)) + if (!isset(self::$tz_cache['UTC'])) { - $utc = calendar_timezones::DateTimeZone('UTC'); + self::$tz_cache['UTC'] = calendar_timezones::DateTimeZone('UTC'); } + $utc = self::$tz_cache['UTC']; + if ($this->type == self::NONE) return false; // no recuring event if ($version == '1.0') @@ -609,13 +625,24 @@ class calendar_rrule implements Iterator * * @param array $event * @param boolean $usertime=true true: event timestamps are usertime (default for calendar_bo::(read|search), false: servertime + * @param string $to_tz timezone for exports (null for event's timezone) + * * @return calendar_rrule */ - public static function event2rrule(array $event,$usertime=true) + public static function event2rrule(array $event,$usertime=true,$to_tz=null) { + if (!$to_tz) $to_tz = $event['tzid']; $timestamp_tz = $usertime ? egw_time::$user_timezone : egw_time::$server_timezone; $time = is_a($event['start'],'DateTime') ? $event['start'] : new egw_time($event['start'],$timestamp_tz); - $time->setTimezone(new DateTimeZone($event['tzid'])); + + if (!isset(self::$tz_cache[$to_tz])) + { + self::$tz_cache[$to_tz] = calendar_timezones::DateTimeZone($to_tz); + } + + self::rrule2tz($event, $time, $to_tz); + + $time->setTimezone(self::$tz_cache[$to_tz]); if ($event['recur_enddate']) { @@ -646,6 +673,64 @@ class calendar_rrule implements Iterator 'recur_exception' => $this->exceptions, ); } + + /** + * Shift a recurrence rule to a new timezone + * + * @param array $event recurring event + * @param DateTime/string starttime of the event (in servertime) + * @param string $to_tz new timezone + */ + public static function rrule2tz(array &$event,$starttime,$to_tz) + { + // We assume that the difference between timezones can result + // in a maximum of one day + + if (!is_array($event) || + !isset($event['recur_type']) || + $event['recur_type'] == MCAL_RECUR_NONE || + empty($event['recur_data']) || $event['recur_data'] == ALLDAYS || + empty($event['tzid']) || empty($to_tz) || + $event['tzid'] == $to_tz) return; + + if (!isset(self::$tz_cache[$event['tzid']])) + { + self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); + } + if (!isset(self::$tz_cache[$to_tz])) + { + self::$tz_cache[$to_tz] = calendar_timezones::DateTimeZone($to_tz); + } + + $time = is_a($starttime,'DateTime') ? + $starttime : new egw_time($starttime, egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $remote = clone $time; + $remote->setTimezone(self::$tz_cache[$to_tz]); + $delta = (int)$remote->format('w') - (int)$time->format('w'); + if ($delta) + { + // We have to generate a shifted rrule + switch ($event['recur_type']) + { + case self::MONTHLY_WDAY: + case self::WEEKLY: + $mask = (int)$event['recur_data']; + + if ($delta == 1 || $delta == -6) + { + $mask = $mask << 1; + if ($mask & 128) $mask = $mask - 127; // overflow + } + else + { + if ($mask & 1) $mask = $mask + 128; // underflow + $mask = $mask >> 1; + } + $event['recur_data'] = $mask; + } + } + } } if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) // some tests diff --git a/calendar/inc/class.calendar_sif.inc.php b/calendar/inc/class.calendar_sif.inc.php index 56b6b58c05..c0edad27a7 100644 --- a/calendar/inc/class.calendar_sif.inc.php +++ b/calendar/inc/class.calendar_sif.inc.php @@ -195,7 +195,7 @@ class calendar_sif extends calendar_boupdate * @param string $tzid TZID of event or 'UTC' or NULL for palmos timestamps in usertime * @return mixed attribute value to set: integer timestamp if $tzid == 'UTC' otherwise Ymd\THis string IN $tzid */ - function getDateTime($time,$tzid) + function getDateTime($time, $tzid) { if (empty($tzid) || $tzid == 'UTC') { @@ -223,19 +223,10 @@ class calendar_sif extends calendar_boupdate if ($this->tzid) { // enforce device settings + date_default_timezone_set($this->tzid); $finalEvent['tzid'] = $this->tzid; } - else - { - $finalEvent['tzid'] = egw_time::$user_timezone->getName(); // default to user timezone - } - #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); @@ -248,20 +239,24 @@ 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))); + if ($this->tzid) + { + 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); + /* if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "() $key => $value\n",3,$this->logfile); } - + */ switch ($key) { case 'alldayevent': @@ -269,17 +264,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; @@ -293,7 +288,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; @@ -321,7 +316,14 @@ class calendar_sif extends calendar_boupdate $finalEvent['recur_data'] = 0; if ($this->event['recur_noenddate'] == 0) { - $finalEvent['recur_enddate'] = $this->vCalendar->_parseDateTime($this->event['recur_enddate']); + $recur_enddate = $this->vCalendar->_parseDateTime($this->event['recur_enddate']); + $finalEvent['recur_enddate'] = mktime( + date('H', 23), + date('i', 59), + date('s', 59), + date('m', $recur_enddate), + date('d', $recur_enddate), + date('Y', $recur_enddate)); } switch ($this->event['recur_type']) { @@ -382,6 +384,15 @@ class calendar_sif extends calendar_boupdate } } + if ($this->calendarOwner) $finalEvent['owner'] = $this->calendarOwner; + + if ($this->tzid) + { + 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" . @@ -393,14 +404,15 @@ class calendar_sif extends calendar_boupdate 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; } @@ -410,9 +422,11 @@ 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) { if ($this->log) { @@ -424,24 +438,167 @@ class calendar_sif extends calendar_boupdate return false; } - if (isset($event['alarm'])) + if ($event['recur_type'] != MCAL_RECUR_NONE) { - $alarmData = array(); - $alarmData['offset'] = $event['alarm'] * 60; - $alarmData['time'] = $event['start'] - $alarmData['offset']; - $alarmData['owner'] = $this->user; - $alarmData['all'] = false; - $event['alarm'] = $alarmData; + // Adjust the event start -- no exceptions before and at the start + $length = $event['end'] - $event['start']; + $rriter = calendar_rrule::event2rrule($event, false); + $rriter->rewind(); + if (!$rriter->valid()) continue; // completely disolved into exceptions + + $newstart = egw_time::to($rriter->current, 'server'); + if ($newstart != $event['start']) + { + // leading exceptions skiped + $event['start'] = $newstart; + $event['end'] = $newstart + $length; + } + + $exceptions = $event['recur_exception']; + foreach($exceptions as $key => $day) + { + // remove leading exceptions + if ($day <= $event['start']) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(): event SERIES-MASTER skip leading exception ' . + $day . "\n",3,$this->logfile); + } + unset($exceptions[$key]); + } + } + $event['recur_exception'] = $exceptions; } - if ($_calID > 0 && ($storedEvent = $this->read($_calID))) + if ($recur_date) $event['recurrence'] = $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; - // delete existing alarms - if (count($storedEvent['alarm']) > 0) + if (empty($event['uid'])) { - foreach ($storedEvent['alarm'] as $alarm_id => $alarm_data) + $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 (!empty($value)) $event[$key] = $value; + } + } + else + { + // not merge + // SIF clients do not support participants => add them back + $event['participants'] = $event_info['stored_event']['participants']; + $event['participant_types'] = $event_info['stored_event']['participant_types']; + if ($event['whole_day'] && $event['tzid'] != $event_info['stored_event']['tzid']) + { + if (!isset(self::$tz_cache[$event_info['stored_event']['tzid']])) + { + self::$tz_cache[$event_info['stored_event']['tzid']] = + calendar_timezones::DateTimeZone($event_info['stored_event']['tzid']); + } + // Adjust dates to original TZ + $time = new egw_time($event['startdate'],self::$tz_cache[$event_info['stored_event']['tzid']]); + $event['start'] = egw_time::to($time, 'server'); + $time = new egw_time($event['enddate'],self::$tz_cache[$event_info['stored_event']['tzid']]); + $event['end'] = egw_time::to($time, 'server'); + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + foreach ($event['recur_exception'] as $key => $day) + { + $time = new egw_time($day,egw_time::$server_timezone); + $time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']); + $event['recur_exception'][$key] = egw_time::to($time,'server'); + } + } + } + + calendar_rrule::rrule2tz($event, $event_info['stored_event']['start'], + $event_info['stored_event']['tzid']); + + $event['tzid'] = $event_info['stored_event']['tzid']; + // avoid that iCal changes the organizer, which is not allowed + $event['owner'] = $event_info['stored_event']['owner']; + } + } + else // common adjustments for new events + { + // set non blocking all day depending on the user setting + if (isset($event['whole_day']) + && $event['whole_day'] + && $this->nonBlockingAllday) + { + $event['non_blocking'] = 1; + } + + // check if an owner is set and the current user has add rights + // for that owners calendar; if not set the current user + if (!isset($event['owner']) + || !$this->check_perms(EGW_ACL_ADD, 0, $event['owner'])) + { + $event['owner'] = $this->user; + } + + $status = $event['owner'] == $this->user ? 'A' : 'U'; + $status = calendar_so::combine_status($status, 1, 'CHAIR'); + $event['participants'] = array($event['owner'] => $status); + } + + unset($event['startdate']); + unset($event['enddate']); + + $alarmData = array(); + if (isset($event['alarm'])) + { + $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) @@ -451,327 +608,609 @@ class calendar_sif extends calendar_boupdate } } } - else + + // save event depending on the given event type + switch ($event_info['type']) { - if (isset($event['whole_day']) - && $event['whole_day'] - && $this->nonBlockingAllday) - { - $event['non_blocking'] = 1; - } + case 'SINGLE': + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SINGLE\n",3,$this->logfile); + } + + // update the event + if ($event_info['acl_edit']) + { + // Force SINGLE + unset($event['recurrence']); + $event['reference'] = 0; + $event_to_store = $event; // prevent $event from being changed by the update method + $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); + } + + // remove all known pseudo exceptions and update the event + if ($event_info['acl_edit']) + { + $days = $this->so->get_recurrence_exceptions($event_info['stored_event'], $this->tzid, 0, 0, 'tz_map'); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS MAPPING):\n" . + array2string($days)."\n",3,$this->logfile); + } + if (is_array($days)) + { + $exceptions = array(); + foreach ($event['recur_exception'] as $recur_exception) + { + if (isset($days[$recur_exception])) + { + $exceptions[] = $days[$recur_exception]; + } + } + $event['recur_exception'] = $exceptions; + } + + $event_to_store = $event; // prevent $event from being changed by the update method + $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['recurrence']))); + + // Adjust the event start -- must not be an exception + $length = $event_info['master_event']['end'] - $event_info['master_event']['start']; + $rriter = calendar_rrule::event2rrule($event_info['master_event'], false); + $rriter->rewind(); + if ($rriter->valid()) + { + $newstart = egw_time::to($rriter->current, 'server'); + foreach($event_info['master_event']['recur_exception'] as $key => $day) + { + // remove leading exceptions + if ($day < $newstart) + { + unset($event_info['master_event']['recur_exception'][$key]); + } + } + } + if ($event_info['master_event']['start'] < $newstart) + { + $event_info['master_event']['start'] = $newstart; + $event_info['master_event']['end'] = $newstart + $length; + $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method + $this->server2usertime($event_to_store); + $this->update($event_to_store, true); + unset($event_to_store); + } + $event['reference'] = $event_info['master_event']['id']; + $event['category'] = $event_info['master_event']['category']; + $event['owner'] = $event_info['master_event']['owner']; + } + + $event_to_store = $event; // prevent $event from being changed by update method + $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['recurrence']) + { + $recur_exceptions[] = $recur_exception; + } + } + $event_info['master_event']['recur_exception'] = $recur_exceptions; + + // save the series master with the adjusted exceptions + $event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method + $updated_id = $this->update($event_to_store, true, true, false, false); + unset($event_to_store); + } } - $eventID = $this->update($event, true); - - if ($eventID && $this->log) + // read stored event into info array for fresh stored (new) events + if (!is_array($event_info['stored_event']) && $updated_id > 0) { - $storedEvent = $this->read($eventID); + $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['recurrence'] : 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['recurrence']; + } + 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($storedEvent)."\n",3,$this->logfile); + array2string($event_info['stored_event'])."\n",3,$this->logfile); } - return $eventID; + 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); + $tzid = null; - #$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->log) + if ($this->read($_id, $recur_date, true, 'server')) { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . - array2string($event)."\n",3,$this->logfile); - } - if ($this->uidExtension) - { - if (!preg_match('/\[UID:.+\]/m', $event['description'])) + $retval = -1; // Permission denied + if($this->xmlrpc) { - $event['description'] .= "\n[UID:" . $event['uid'] . "]"; + $GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['no_access'], + $GLOBALS['xmlrpcstr']['no_access']); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() User does not have the permission to read event $_id.\n", + 3,$this->logfile); } - } - - if ($this->tzid === false) - { - $tzid = null; - } - elseif ($this->tzid) - { - $tzid = $this->tzid; } else { - $tzid = $event['tzid']; - } - if ($tzid && $tzid != 'UTC') - { - if (!isset(self::$tz_cache[$tzid])) + $retval = false; // Entry does not exist + if ($this->log) { - self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() Event $_id not found.\n",3,$this->logfile); } } - if (!isset(self::$tz_cache[$event['tzid']])) + return $retval; + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } + + if ($this->tzid) + { + // explicit device timezone + $tzid = $this->tzid; + } + else + { + // use event's timezone + $tzid = $event['tzid']; + } + + if ($this->so->isWholeDay($event)) $event['whole_day'] = true; + + if ($tzid) + { + if (!isset(self::$tz_cache[$tzid])) { - self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); } + } + if (!isset(self::$tz_cache[$event['tzid']])) + { + self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); + } - $sifEvent = self::xml_decl . "" . self::SIF_decl; - - foreach ($this->sifMapping as $sifField => $egwField) + if ($recur_date && ($master = $this->read($_id, 0, true, 'server'))) + { + $days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'tz_rrule'); + if (isset($days[$recur_date])) { - 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) + $recur_date = $days[$recur_date]; // use remote representation + } + else + { + if ($this->log) { - case 'Importance': - $value = $value-1; - $sifEvent .= "<$sifField>$value"; - break; - - case 'RecurrenceType': - case 'Interval': - case 'PatternStartDate': - case 'NoEndDate': - case 'DayOfWeekMask': - case 'PatternEndDate': - break; - - case 'IsRecurring': - if ($event['recur_type'] == MCAL_RECUR_NONE) - { - $sifEvent .= "<$sifField>0"; - break; - } - if ($event['recur_enddate'] == 0) - { - $sifEvent .= '1'; - } - else - { - $time = new egw_time($event['recur_enddate'],egw_time::$server_timezone); - // all calculations in the event's timezone - $time->setTimezone(self::$tz_cache[$event['tzid']]); - $time->setTime(23, 59, 59); - $recurEndDate = $this->date2ts($time); - $sifEvent .= '0'; - $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurEndDate) .''; - } - $time = new egw_time($event['start'],egw_time::$server_timezone); - // all calculations in the event's timezone - $time->setTimezone(self::$tz_cache[$event['tzid']]); - $time->setTime(0, 0, 0); - $recurStartDate = $this->date2ts($time); - $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); - switch ($event['recur_type']) - { - - case MCAL_RECUR_DAILY: - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursDaily .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; - if ($event['recur_enddate']) - { - $totalDays = ($recurEndDate - $recurStartDate) / 86400; - $occurrences = ceil($totalDays / $eventInterval); - $sifEvent .= ''. $occurrences .''; - } - break; - - case MCAL_RECUR_WEEKLY: - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursWeekly .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; - $sifEvent .= ''. $event['recur_data'] .''; - if ($event['recur_enddate']) - { - $daysPerWeek = substr_count(decbin($event['recur_data']),'1'); - $totalWeeks = floor(($recurEndDate - $recurStartDate) / (86400*7)); - $occurrences = ($totalWeeks / $eventInterval) * $daysPerWeek; - for($i = $recurEndDate; $i > $recurStartDate + ($totalWeeks * 86400*7); $i = $i - 86400) - { - switch (date('w', $i-1)) - { - case 0: - if ($event['recur_data'] & 1) $occurrences++; - break; - // monday - case 1: - if ($event['recur_data'] & 2) $occurrences++; - break; - case 2: - if ($event['recur_data'] & 4) $occurrences++; - break; - case 3: - if ($event['recur_data'] & 8) $occurrences++; - break; - case 4: - if ($event['recur_data'] & 16) $occurrences++; - break; - case 5: - if ($event['recur_data'] & 32) $occurrences++; - break; - case 6: - if ($event['recur_data'] & 64) $occurrences++; - break; - } - } - $sifEvent .= ''. $occurrences .''; - } - break; - - case MCAL_RECUR_MONTHLY_MDAY: - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursMonthly .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; - break; - - case MCAL_RECUR_MONTHLY_WDAY: - $weekMaskMap = array('Sun' => self::olSunday, 'Mon' => self::olMonday, 'Tue' => self::olTuesday, - 'Wed' => self::olWednesday, 'Thu' => self::olThursday, 'Fri' => self::olFriday, - 'Sat' => self::olSaturday); - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursMonthNth .''; - $sifEvent .= ''. $eventInterval .''; - $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; - $sifEvent .= '' . (1 + (int) ((date('d',$event['start'])-1) / 7)) . ''; - $sifEvent .= '' . $weekMaskMap[date('D',$event['start'])] . ''; - break; - - case MCAL_RECUR_YEARLY: - $sifEvent .= "<$sifField>1"; - $sifEvent .= ''. self::olRecursYearly .''; - break; - } - if (is_array($event['recur_exception'])) - { - $sifEvent .= ''; - foreach ($event['recur_exception'] as $day) - { - if ($this->isWholeDay($event)) - { - $time = new egw_time($day,egw_time::$server_timezone); - $time->setTimezone(self::$tz_cache[$tzid]); - $sifEvent .= '' . $time->format('Y-m-d') . ''; - } - else - { - $sifEvent .= '' . self::getDateTime($day,$tzid) . ''; - } - } - $sifEvent .= ''; - } - break; - - case 'Sensitivity': - $value = (!$value ? '2' : '0'); - $sifEvent .= "<$sifField>$value"; - break; - - case 'Folder': - # skip currently. This is the folder where Outlook stores the contact. - #$sifEvent .= "<$sifField>/"; - break; - - case 'AllDayEvent': - case 'End': - // get's handled by Start clause - break; - - case 'Start': - if ($this->isWholeDay($event)) - { - $time = new egw_time($event['start'],egw_time::$server_timezone); - $time->setTimezone(self::$tz_cache[$tzid]); - $sifEvent .= '' . $time->format('Y-m-d') . ''; - $time = new egw_time($event['end'],egw_time::$server_timezone); - $time->setTimezone(self::$tz_cache[$tzid]); - $sifEvent .= '' . $time->format('Y-m-d') . ''; - $sifEvent .= "1"; - } - else - { - $sifEvent .= '' . self::getDateTime($event['start'],$tzid) . ''; - $sifEvent .= '' . self::getDateTime($event['end'],$tzid) . ''; - $sifEvent .= "0"; - } - break; - - case 'ReminderMinutesBeforeStart': - break; - - case 'ReminderSet': - if (count((array)$event['alarm']) > 0) - { - $sifEvent .= "<$sifField>1"; - foreach ($event['alarm'] as $alarmID => $alarmData) - { - $sifEvent .= ''. $alarmData['offset']/60 .''; - // lets take only the first alarm - break; - } - } - else - { - $sifEvent .= "<$sifField>0"; - } - break; - - case 'Categories': - if (!empty($value) && ($values = $this->get_categories($value))) - { - $value = implode(', ', $values); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - else - { - break; - } - - default: - $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); - $sifEvent .= "<$sifField>$value"; - break; + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($_id, $recur_date) Unsupported status only exception, skipped ...\n", + 3,$this->logfile); } + return false; // unsupported pseudo exception } - $sifEvent .= ""; - + /* + $time = new egw_time($master['start'], egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$tzid]); + $first_start = $time->format('His'); + $time = new egw_time($event['start'], egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$tzid]); + $recur_start = $time->format('His'); + if ($first_start == $recur_start) return false; // Nothing to export + */ + $event['recur_type'] = MCAL_RECUR_NONE; + } + elseif (!$recur_date && + $event['recur_type'] != MCAL_RECUR_NONE && + !isset($event['whole_day'])) // whole-day events are not shifted + { + // Add the timezone transition related pseudo exceptions + $exceptions = $this->so->get_recurrence_exceptions($event, $tzid, 0, 0, 'tz_rrule'); 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); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS)\n" . + array2string($exceptions)."\n",3,$this->logfile); } + $event['recur_exception'] = $exceptions; + // Adjust the event start -- must not be an exception + $length = $event['end'] - $event['start']; + $rriter = calendar_rrule::event2rrule($event, false, $tzid); + $rriter->rewind(); + if (!$rriter->valid()) return false; // completely disolved into exceptions - return $sifEvent; + $event['start'] = egw_time::to($rriter->current, 'server'); + $event['end'] = $event['start'] + $length; + foreach($exceptions as $key => $day) + { + // remove leading exceptions + if ($day <= $event['start']) unset($exceptions[$key]); + } + $event['recur_exception'] = $exceptions; + calendar_rrule::rrule2tz($event, $event['start'], $tzid); } - if($this->xmlrpc) + if ($this->uidExtension) { - $GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['no_access'],$GLOBALS['xmlrpcstr']['no_access']); + if (!preg_match('/\[UID:.+\]/m', $event['description'])) + { + $event['description'] .= "\n[UID:" . $event['uid'] . "]"; + } } - return False; + + $sifEvent = self::xml_decl . "" . self::SIF_decl; + + foreach ($this->sifMapping as $sifField => $egwField) + { + if (empty($egwField)) continue; + + $value = $GLOBALS['egw']->translation->convert($event[$egwField], $sysCharSet, 'utf-8'); + + switch ($sifField) + { + case 'Importance': + $value = $value-1; + $sifEvent .= "<$sifField>$value"; + break; + + case 'RecurrenceType': + case 'Interval': + case 'PatternStartDate': + case 'NoEndDate': + case 'DayOfWeekMask': + case 'PatternEndDate': + break; + + case 'IsRecurring': + if ($event['recur_type'] == MCAL_RECUR_NONE) + { + $sifEvent .= "<$sifField>0"; + break; + } + if ($event['recur_enddate'] == 0) + { + $sifEvent .= '1'; + } + else + { + $time = new egw_time($event['recur_enddate'],egw_time::$server_timezone); + // all calculations in the event's timezone + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $time->setTime(23, 59, 59); + + if ($tzid) + { + $time->setTimezone(self::$tz_cache[$tzid]); + } + else + { + $time->setTimezone(egw_time::$user_timezone); + } + $recurEndDate = egw_time::to($time,'server'); + $sifEvent .= '0'; + $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurEndDate) .''; + } + + $time = new egw_time($event['start'],egw_time::$server_timezone); + + // all calculations in the event's timezone + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $time->setTime(0, 0, 0); + + if ($tzid) + { + $time->setTimezone(self::$tz_cache[$tzid]); + } + else + { + $time->setTimezone(egw_time::$user_timezone); + } + $recurStartDate = egw_time::to($time,'server'); + $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); + + switch ($event['recur_type']) + { + + case MCAL_RECUR_DAILY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursDaily .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; + if ($event['recur_enddate']) + { + $totalDays = ($recurEndDate - $recurStartDate) / 86400; + $occurrences = ceil($totalDays / $eventInterval); + $sifEvent .= ''. $occurrences .''; + } + break; + + case MCAL_RECUR_WEEKLY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursWeekly .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; + $sifEvent .= ''. $event['recur_data'] .''; + if ($event['recur_enddate']) + { + $daysPerWeek = substr_count(decbin($event['recur_data']),'1'); + $totalWeeks = floor(($recurEndDate - $recurStartDate) / (86400*7)); + $occurrences = ceil($totalWeeks / $eventInterval) * $daysPerWeek; + for($i = $recurEndDate; $i > $recurStartDate + ($totalWeeks * 86400*7); $i = $i - 86400) + { + switch (date('w', $i-1)) + { + case 0: + if ($event['recur_data'] & 1) $occurrences++; + break; + // monday + case 1: + if ($event['recur_data'] & 2) $occurrences++; + break; + case 2: + if ($event['recur_data'] & 4) $occurrences++; + break; + case 3: + if ($event['recur_data'] & 8) $occurrences++; + break; + case 4: + if ($event['recur_data'] & 16) $occurrences++; + break; + case 5: + if ($event['recur_data'] & 32) $occurrences++; + break; + case 6: + if ($event['recur_data'] & 64) $occurrences++; + break; + } + } + $sifEvent .= ''. $occurrences .''; + } + break; + + case MCAL_RECUR_MONTHLY_MDAY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursMonthly .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; + break; + + case MCAL_RECUR_MONTHLY_WDAY: + $weekMaskMap = array('Sun' => self::olSunday, 'Mon' => self::olMonday, 'Tue' => self::olTuesday, + 'Wed' => self::olWednesday, 'Thu' => self::olThursday, 'Fri' => self::olFriday, + 'Sat' => self::olSaturday); + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursMonthNth .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. $this->vCalendar->_exportDateTime($recurStartDate) .''; + $sifEvent .= '' . (1 + (int) ((date('d',$event['start'])-1) / 7)) . ''; + $sifEvent .= '' . $weekMaskMap[date('D',$event['start'])] . ''; + break; + + case MCAL_RECUR_YEARLY: + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursYearly .''; + break; + } + if (is_array($event['recur_exception'])) + { + $sifEvent .= ''; + foreach ($event['recur_exception'] as $day) + { + if (isset($event['whole_day'])) + { + if (!is_a($day,'DateTime')) + { + $day = new egw_time($day,egw_time::$server_timezone); + $day->setTimezone(self::$tz_cache[$event['tzid']]); + } + $sifEvent .= '' . $day->format('Y-m-d') . ''; + } + else + { + if ($this->log && is_a($day,'DateTime')) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() exception[' . $day->getTimezone()->getName() . ']: ' . + $day->format('Ymd\THis') . "\n",3,$this->logfile); + } + $sifEvent .= '' . self::getDateTime($day,$tzid) . ''; + } + } + $sifEvent .= ''; + } + break; + + case 'Sensitivity': + $value = (!$value ? '2' : '0'); + $sifEvent .= "<$sifField>$value"; + break; + + case 'Folder': + # skip currently. This is the folder where Outlook stores the contact. + #$sifEvent .= "<$sifField>/"; + break; + + case 'AllDayEvent': + case 'End': + // get's handled by Start clause + break; + + case 'Start': + if (isset($event['whole_day'])) + { + // for whole-day events we use the date in event timezone + $time = new egw_time($event['start'],egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $sifEvent .= '' . $time->format('Y-m-d') . ''; + $time = new egw_time($event['end'],egw_time::$server_timezone); + $time->setTimezone(self::$tz_cache[$event['tzid']]); + $sifEvent .= '' . $time->format('Y-m-d') . ''; + $sifEvent .= "1"; + } + else + { + $sifEvent .= '' . self::getDateTime($event['start'],$tzid) . ''; + $sifEvent .= '' . self::getDateTime($event['end'],$tzid) . ''; + $sifEvent .= "0"; + } + break; + + case 'ReminderMinutesBeforeStart': + break; + + case 'ReminderSet': + if (count((array)$event['alarm']) > 0) + { + $sifEvent .= "<$sifField>1"; + foreach ($event['alarm'] as $alarmID => $alarmData) + { + $sifEvent .= ''. $alarmData['offset']/60 .''; + // lets take only the first alarm + break; + } + } + else + { + $sifEvent .= "<$sifField>0"; + } + break; + + case 'Categories': + if (!empty($value) && ($values = $this->get_categories($value))) + { + $value = implode(', ', $values); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } + else + { + break; + } + + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); + $sifEvent .= "<$sifField>$value"; + } + } + $sifEvent .= ""; + + 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; } /** diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index f51c14a6ad..131e5deca8 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -157,6 +157,7 @@ class calendar_so // We want only the parents to match $where['cal_uid'] = $ids; $where['cal_reference'] = 0; + $where['cal_recurrence'] = 0; } if ((int) $recur_date) { @@ -358,7 +359,7 @@ class calendar_so 'cal_user_type' => $type, 'cal_user_id' => $ids, )); - if ($type == 'u' && ($filter == 'owner' || $filter == 'all')) + if ($type == 'u' && ($filter == 'owner')) { $cal_table_def = $this->db->get_table_definitions('calendar',$this->cal_table); $to_or[] = $this->db->expression($cal_table_def,array('cal_owner' => $ids)); @@ -377,6 +378,7 @@ class calendar_so case 'rejected': $where[] = "cal_status='R'"; break; case 'all': + case 'owner': break; default: //if (!$show_rejected) // not longer used @@ -458,22 +460,22 @@ class calendar_so $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['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(); $events[$id] = egw_db::strip_array_keys($row,'cal_'); } //_debug_array($events); - if (count($ids)) + if (count($events)) { // now ready all users with the given cal_id AND (cal_recur_date=0 or the fitting recur-date) // This will always read the first entry of each recuring event too, we eliminate it later @@ -537,7 +539,7 @@ class calendar_so //echo "

socal::search\n"; _debug_array($events); return $events; } - + /** * Data returned by calendar_search_union hook */ @@ -558,11 +560,11 @@ class calendar_so { if (in_array(basename($_SERVER['SCRIPT_FILENAME']),array('groupdav.php','rpc.php','xmlrpc.php'))) { - return; // disable integration for GroupDAV, SyncML, ... + return; // disable integration for GroupDAV, SyncML, ... } self::$integration_data = $GLOBALS['egw']->hooks->process(array( 'location' => 'calendar_search_union', - 'cols' => $selects[0]['cols'], // cols to return + 'cols' => $selects[0]['cols'], // cols to return 'start' => $start, 'end' => $end, 'users' => $users, @@ -579,17 +581,17 @@ class calendar_so } } } - + /** * Get data from last 'calendar_search_union' hook call - * + * * @return array */ public static function get_integration_data() { return self::$integration_data; } - + /** * Checks for conflicts */ @@ -669,7 +671,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) { @@ -1565,95 +1579,334 @@ ORDER BY cal_user_type, cal_usre_id * @param array $event Recurring Event. * @param string tz_id=null timezone for exports (null for event's timezone) * @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 string $filter='all' string filter-name: all (not rejected), accepted, unknown, tentative, - * rejected, tz_transitions (return only by timezone transition affected entries) + * @param int $end=0 if != 0: enddate of the search/list (servertime) + * @param string $filter='all' string filter-name: all (not rejected), + * accepted, unknown, tentative, rejected, + * rrule return array of remote exceptions in servertime + * tz_rrule/tz_only, return (only by) timezone transition affected entries + * map return array of dates with no pseudo exception + * key remote occurrence date + * tz_map return array of all dates with no tz pseudo exception * * @return array Array of exception days (false for non-recurring events). */ - function get_recurrence_exceptions(&$event, $tz_id=null, $start=0, $end=0, $filter='all') + function get_recurrence_exceptions($event, $tz_id=null, $start=0, $end=0, $filter='all') { + if (!is_array($event)) return false; $cal_id = (int) $event['id']; + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // "($cal_id, $tz_id, $filter): " . $event['tzid']); if (!$cal_id || $event['recur_type'] == MCAL_RECUR_NONE) return false; $days = array(); - $first_start = $recur_start = ''; - if (!empty($tz_id)) + $expand_all = (!$this->isWholeDay($event) && $tz_id && $tz_id != $event['tzid']); + + if ($filter == 'tz_only' && !$expand_all) return $days; + + $remote = in_array($filter, array('tz_rrule', 'rrule')); + + $egw_rrule = calendar_rrule::event2rrule($event, false); + $egw_rrule->rewind(); + if ($expand_all) { - // set export timezone - if(!isset(self::$tz_cache[$tz_id])) + unset($event['recur_excpetion']); + $remote_rrule = calendar_rrule::event2rrule($event, false, $tz_id); + $remote_rrule->rewind(); + } + while ($egw_rrule->valid()) + { + $day = $egw_rrule->current(); + $locts = (int)egw_time::to($day,'server'); + $tz_exception = ($filter == 'tz_rrule'); + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // '()[EVENT Server]: ' . $day->format('Ymd\THis') . " ($locts)"); + if ($expand_all) { - self::$tz_cache[$tz_id] = calendar_timezones::DateTimeZone($tz_id); + $remote_day = $remote_rrule->current(); + $remts = (int)egw_time::to($remote_day,'server'); + // error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // '()[EVENT Device]: ' . $remote_day->format('Ymd\THis') . " ($remts)"); } - $starttime = new egw_time($event['start'], egw_time::$server_timezone); - $starttime->setTimezone(self::$tz_cache[$tz_id]); - $first_start = $starttime->format('His'); + + + if (!($end && $end < $locts) && $start <= $locts) + { + // we are within the relevant time period + if ($expand_all && $day->format('U') != $remote_day->format('U')) + { + $tz_exception = true; + if ($filter != 'map' && $filter != 'tz_map') + { + // timezone pseudo exception + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // '() tz exception: ' . $day->format('Ymd\THis')); + if ($remote) + { + $days[$locts]= $remts; + } + else + { + $days[$remts]= $locts; + } + } + } + if ($filter != 'tz_map' && (!$tz_exception || $filter == 'tz_only') && + $this->status_pseudo_exception($event['id'], $locts, $filter)) + { + // status pseudo exception + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // '() status exception: ' . $day->format('Ymd\THis')); + if ($expand_all) + { + $remts = (int)egw_time::to($remote_day,'server'); + if ($filter == 'tz_only') + { + unset($days[$remts]); + } + else + { + if ($filter != 'map') + { + if ($remote) + { + $days[$locts]= $remts; + } + else + { + $days[$remts]= $locts; + } + } + } + } + elseif ($filter != 'map') + { + $days[$locts]= $locts; + } + } + elseif (($filter == 'map' || filter == 'tz_map') && + !$tz_exception) + { + // no pseudo exception date + if ($expand_all) + { + + $days[$remts]= $locts; + } + else + { + $days[$locts]= $locts; + } + } + } + do + { + $egw_rrule->next_no_exception(); + $day = $egw_rrule->current(); + if ($expand_all) + { + $remote_rrule->next_no_exception(); + $remts = (int)egw_time::to($remote_rrule->current(),'server'); + } + $exception = $egw_rrule->exceptions && + in_array($day->format('Ymd'),$egw_rrule->exceptions); + if (in_array($filter, array('map','tz_map','rrule','tz_rrule')) + && $exception) + { + // real exception + $locts = (int)egw_time::to($day,'ts'); + if ($expand_all) + { + if ($remote) + { + $days[$locts]= $remts; + } + else + { + $days[$remts]= $locts; + } + } + else + { + $days[$locts]= $locts; + } + } + } + while ($exception); + } + return $days; + } + + /** + * Checks for status only pseudo exceptions + * + * @param int $cal_id event id + * @param int $recur_date occurrence to check + * @param string $filter status filter criteria for user + * + * @return boolean true, if stati don't match with defaults + */ + function status_pseudo_exception($cal_id, $recur_date, $filter) + { + static $recurrence_zero; + static $cached_id; + static $user; + + if (!isset($cached_id) || $cached_id != $cal_id) + { + // get default stati + $recurrence_zero = array(); + $user = $GLOBALS['egw_info']['user']['account_id']; + $where = array('cal_id' => $cal_id, + 'cal_recur_date' => 0); + foreach ($this->db->select($this->user_table,'cal_user_id,cal_user_type,cal_status',$where, + __LINE__,__FILE__,false,'','calendar') as $row) + { + switch ($row['cal_user_type']) + { + case 'u': // account + case 'c': // contact + case 'e': // email address + $uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']); + $recurrence_zero[$uid] = $row['cal_status']; + } + } + $cached_id = $cal_id; } - $participants = $this->get_participants($event['id'], 0); + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // "($cal_id, $recur_date, $filter)[DEFAULTS]: " . + // array2string($recurrence_zero)); - // Check if the stati for all participants are identical for all recurrences - foreach ($participants as $uid => $attendee) + $participants = array(); + $where = array('cal_id' => $cal_id, + 'cal_recur_date' => $recur_date); + foreach ($this->db->select($this->user_table,'cal_user_id,cal_user_type,cal_status',$where, + __LINE__,__FILE__,false,'','calendar') as $row) { - switch ($attendee['type']) + switch ($row['cal_user_type']) { case 'u': // account case 'c': // contact case 'e': // email address - $recurrences = $this->get_recurrences($event['id'], $uid, $start, $end); - foreach ($recurrences as $recur_date => $recur_status) - { - if ($recur_date) - { - if (!empty($tz_id)) - { - $time = new egw_time($recur_date, egw_time::$server_timezone); - $time->setTimezone(self::$tz_cache[$tz_id]); - $recur_start = $time->format('His'); - //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. - // "() first=$first_start <> current=$recur_start"); - } - if ($attendee['type'] = 'u' && - $attendee['id'] == $GLOBALS['egw_info']['user']['account_id']) - { - // handle filter for current user - switch ($filter) - { - case 'unknown': - if ($recur_status != 'U') continue; - break; - case 'accepted': - if ($recur_status != 'A') continue; - break; - case 'tentative': - if ($recur_status != 'T') continue; - break; - case 'rejected': - if ($recur_status != 'R') continue; - break; - case 'default': - if ($recur_status == 'R') continue; - break; - default: - // All entries - } - } - if (($filter != 'tz_transitions' && $recur_status != $recurrences[0]) - || !empty($tz_id) && $first_start != $recur_start) - { - // Every distinct status or starttime results in an exception - $days[] = $recur_date; - } - } - } - break; - default: // We don't handle the rest - break; + $uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']); + $participants[$uid] = $row['cal_status']; } } - $days = array_unique($days); - sort($days); - return $days; + + if (empty($participants)) return false; // occurrence does not exist at all yet + + foreach ($recurrence_zero as $uid => $status) + { + if ($uid == $user) + { + // handle filter for current user + switch ($filter) + { + case 'unknown': + if ($status != 'U') + { + unset($participants[$uid]); + continue; + } + break; + case 'accepted': + if ($status != 'A') + { + unset($participants[$uid]); + continue; + } + break; + case 'tentative': + if ($status != 'T') + { + unset($participants[$uid]); + continue; + } + break; + case 'rejected': + if ($status != 'R') + { + unset($participants[$uid]); + continue; + } + break; + case 'default': + if ($status == 'R') + { + unset($participants[$uid]); + continue; + } + break; + default: + // All entries + } + } + if (!isset($participants[$uid]) + || $participants[$uid] != $status) + return true; + unset($participants[$uid]); + } + return (!empty($participants)); + } + + /** + * Check if the event is the whole day + * + * @param array $event event (all timestamps in servertime) + * @return boolean true if whole day event within its timezone, false othwerwise + */ + function isWholeDay($event) + { + if (!isset($event['start']) || !isset($event['end'])) return false; + + if (empty($event['tzid'])) + { + $timezone = egw_time::$server_timezone; + } + else + { + if (!isset(self::$tz_cache[$event['tzid']])) + { + self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); + } + $timezone = self::$tz_cache[$event['tzid']]; + } + $start = new egw_time($event['start'],egw_time::$server_timezone); + $start->setTimezone($timezone); + $end = new egw_time($event['end'],egw_time::$server_timezone); + $end->setTimezone($timezone); + //error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + // '(): ' . $start . '-' . $end); + $start = egw_time::to($start,'array'); + $end = egw_time::to($end,'array'); + + + return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59; + } + + /** + * Moves a datetime to the beginning og the day within timezone + * + * @param egw_time &time the datetime entry + * @param string tz_id timezone + * + * @return DateTime + */ + function &startOfDay(egw_time $time, $tz_id) + { + if (empty($tz_id)) + { + $timezone = egw_time::$server_timezone; + } + else + { + if (!isset(self::$tz_cache[$tz_id])) + { + self::$tz_cache[$tz_id] = calendar_timezones::DateTimeZone($tz_id); + } + $timezone = self::$tz_cache[$tz_id]; + } + return new egw_time($time->format('Y-m-d 00:00:00'), $timezone); } } diff --git a/phpgwapi/inc/horde/Horde/SyncML/State.php b/phpgwapi/inc/horde/Horde/SyncML/State.php index 61c744a551..45f48b866b 100644 --- a/phpgwapi/inc/horde/Horde/SyncML/State.php +++ b/phpgwapi/inc/horde/Horde/SyncML/State.php @@ -277,7 +277,7 @@ class Horde_SyncML_State { $this->setPassword($password); } - $this->_isAuthorized = false; + $this->_isAuthorized = 0; $this->_isAuthConfirmed = false; } @@ -560,12 +560,12 @@ class Horde_SyncML_State { if($GLOBALS['sessionid'] = $GLOBALS['egw']->session->create($this->_locName,$this->_password,'text','u')) { - $this->_isAuthorized = true; + $this->_isAuthorized = 1; #Horde::logMessage('SyncML_EGW: Authentication of ' . $this->_locName . '/' . $GLOBALS['sessionid'] . ' succeded' , __FILE__, __LINE__, PEAR_LOG_DEBUG); } else { - $this->_isAuthorized = false; + $this->_isAuthorized = -1; Horde::logMessage('SyncML: Authentication of ' . $this->_locName . ' failed' , __FILE__, __LINE__, PEAR_LOG_DEBUG); } } @@ -577,7 +577,7 @@ class Horde_SyncML_State { Horde::logMessage('SyncML_EGW: egw session('.$sessionID. ') not verified ' , __FILE__, __LINE__, PEAR_LOG_DEBUG); } - return $this->_isAuthorized; + return ($this->_isAuthorized > 0); } function isAuthConfirmed() diff --git a/phpgwapi/inc/horde/Horde/SyncML/State_egw.php b/phpgwapi/inc/horde/Horde/SyncML/State_egw.php index 26b93d3890..089f0820db 100644 --- a/phpgwapi/inc/horde/Horde/SyncML/State_egw.php +++ b/phpgwapi/inc/horde/Horde/SyncML/State_egw.php @@ -60,6 +60,12 @@ class EGW_SyncML_State extends Horde_SyncML_State $ts = $GLOBALS['egw']->contenthistory->getTSforAction($_appName, $_id, $_action); + if (strstr($_id, ':')) { + // pseudo entries are related to parent entry + $parentId = array_shift(explode(':', $_id)); + $pts = $GLOBALS['egw']->contenthistory->getTSforAction($_appName, $parentId, $_action); + if ($pts > $ts) $ts = $pts; // We have changed the parent + } return $ts; } @@ -336,18 +342,18 @@ class EGW_SyncML_State extends Horde_SyncML_State if (($GLOBALS['sessionid'] = $GLOBALS['egw']->session->create($this->_locName,$this->_password,'text'))) { if ($GLOBALS['egw_info']['user']['apps']['syncml']) { - $this->_isAuthorized = true; + $this->_isAuthorized = 1; Horde::logMessage('SyncML_EGW: Authentication of ' . $this->_locName . '/' . $GLOBALS['sessionid'] . ' succeded', __FILE__, __LINE__, PEAR_LOG_DEBUG); } else { - $this->_isAuthorized = false; + $this->_isAuthorized = -1; // Authentication failed! Horde::logMessage('SyncML is not enabled for user ' . $this->_locName, __FILE__, __LINE__, PEAR_LOG_ERROR); } - return $this->_isAuthorized; + return ($this->_isAuthorized > 0); } } - $this->_isAuthorized = false; + $this->_isAuthorized = -1; Horde::logMessage('SyncML: Authentication of ' . $this->_locName . ' failed' , __FILE__, __LINE__, PEAR_LOG_INFO); } else { @@ -359,7 +365,7 @@ class EGW_SyncML_State extends Horde_SyncML_State __FILE__, __LINE__, PEAR_LOG_WARNING); } } - return $this->_isAuthorized; + return ($this->_isAuthorized > 0); } /** diff --git a/phpgwapi/inc/horde/Horde/SyncML/Sync/RefreshFromServerSync.php b/phpgwapi/inc/horde/Horde/SyncML/Sync/RefreshFromServerSync.php index 1eb07c795b..1575017f19 100644 --- a/phpgwapi/inc/horde/Horde/SyncML/Sync/RefreshFromServerSync.php +++ b/phpgwapi/inc/horde/Horde/SyncML/Sync/RefreshFromServerSync.php @@ -89,6 +89,9 @@ class Horde_SyncML_Sync_RefreshFromServerSync extends Horde_SyncML_Sync_TwoWaySy $contentType = $state->getPreferedContentTypeClient($this->_sourceLocURI, $this->_targetLocURI); $c = $registry->call($hordeType . '/export', array('guid' => $guid, 'contentType' => $contentType)); + + if ($c === false) continue; // no content to export + if (is_a($c, 'PEAR_Error')) { Horde::logMessage("SyncML: refresh failed to export guid $guid:\n" . print_r($c, true), __FILE__, __LINE__, PEAR_LOG_WARNING); diff --git a/phpgwapi/inc/horde/Horde/SyncML/Sync/SlowSync.php b/phpgwapi/inc/horde/Horde/SyncML/Sync/SlowSync.php index 8a9d704f9c..59664cb881 100644 --- a/phpgwapi/inc/horde/Horde/SyncML/Sync/SlowSync.php +++ b/phpgwapi/inc/horde/Horde/SyncML/Sync/SlowSync.php @@ -96,6 +96,9 @@ class Horde_SyncML_Sync_SlowSync extends Horde_SyncML_Sync_TwoWaySync { $contentType = $state->getPreferedContentTypeClient($this->_sourceLocURI, $this->_targetLocURI); $c = $registry->call($hordeType . '/export', array('guid' => $guid, 'contentType' => $contentType)); + + if ($c === false) continue; // no content to export + if (is_a($c, 'PEAR_Error')) { Horde::logMessage("SyncML: slowsync failed to export guid $guid:\n" . print_r($c, true), __FILE__, __LINE__, PEAR_LOG_WARNING); @@ -235,7 +238,7 @@ class Horde_SyncML_Sync_SlowSync extends Horde_SyncML_Sync_TwoWaySync { $guid = false; $guid = $registry->call($hordeType . '/search', - array($state->convertClient2Server($syncItem->getContent(), $contentType), $contentType, $state->getGlobalUID($type, $syncItem->getLocURI()) )); + array($state->convertClient2Server($syncItem->getContent(), $contentType), $contentType, $state->getGlobalUID($type, $syncItem->getLocURI()), $type)); if ($guid) { // Check if the found entry came from the client diff --git a/phpgwapi/inc/horde/Horde/SyncML/Sync/TwoWaySync.php b/phpgwapi/inc/horde/Horde/SyncML/Sync/TwoWaySync.php index d68ba1ccfc..1e8eae1d8d 100644 --- a/phpgwapi/inc/horde/Horde/SyncML/Sync/TwoWaySync.php +++ b/phpgwapi/inc/horde/Horde/SyncML/Sync/TwoWaySync.php @@ -135,6 +135,9 @@ class Horde_SyncML_Sync_TwoWaySync extends Horde_SyncML_Sync { 'guid' => $guid, 'contentType' => $contentType )); + + if ($c === false) continue; // no content to export + if (is_a($c, 'PEAR_Error')) { // Item in history but not in database. Strange, but can happen. Horde :: logMessage("SyncML: change: export of guid $guid failed:\n" . print_r($c, true), @@ -343,13 +346,14 @@ class Horde_SyncML_Sync_TwoWaySync extends Horde_SyncML_Sync { // Create an Add request for client. $contentType = $state->getPreferedContentTypeClient($this->_sourceLocURI, $this->_targetLocURI); - $c = $registry->call($hordeType . '/export', array ( 'guid' => $guid, 'contentType' => $contentType, )); + if ($c === false) continue; // no content to export + if (is_a($c, 'PEAR_Error')) { // Item in history but not in database. Strange, but can happen. Horde :: logMessage("SyncML: add: export of guid $guid failed:\n" . print_r($c, true), @@ -406,7 +410,7 @@ class Horde_SyncML_Sync_TwoWaySync extends Horde_SyncML_Sync { function loadData() { global $registry; - $state = & $_SESSION['SyncML.state']; + $state =& $_SESSION['SyncML.state']; $syncType = $this->_targetLocURI; $hordeType = $state->getHordeType($syncType); $refts = $state->getServerAnchorLast($syncType); @@ -421,15 +425,29 @@ class Horde_SyncML_Sync_TwoWaySync extends Horde_SyncML_Sync { $state->setAddedItems($syncType, $addedItems); - $state->setDeletedItems($syncType, $registry->call($hordeType . '/listBy', array ( + $changedItems =& $state->getChangedItems($syncType); + + $deletedItems =& $registry->call($hordeType . '/listBy', array ( 'action' => 'delete', 'timestamp' => $refts, 'type' => $syncType, 'filter' => $this->_filterExpression - ))); + )); + foreach ($deletedItems as $guid) + { + if (strstr($guid, ':')) + { + $parentGUID = array_shift(explode(':', $guid)); + if (!in_array($parentGUID, $changedItems)) + { + $changedItems[] = $parentGUID; + } + } + } + $state->setDeletedItems($syncType, $deletedItems); - $this->_syncDataLoaded = TRUE; + $this->_syncDataLoaded = true; - return count($state->getChangedItems($syncType)) + count($state->getDeletedItems($syncType)) + count($state->getAddedItems($syncType)) + count($state->getConflictItems($syncType)); + return count($changedItems) + count($deletedItems) + count($addedItems) + count($state->getConflictItems($syncType)); } } \ No newline at end of file diff --git a/phpgwapi/inc/horde/Horde/iCalendar.php b/phpgwapi/inc/horde/Horde/iCalendar.php index 4850c85162..bc81cf3b8a 100644 --- a/phpgwapi/inc/horde/Horde/iCalendar.php +++ b/phpgwapi/inc/horde/Horde/iCalendar.php @@ -735,9 +735,9 @@ class Horde_iCalendar { foreach ($values[1] as $value) { if ((isset($params['VALUE']) && $params['VALUE'] == 'DATE') || (!isset($params['VALUE']) && $isDate)) { - $dates[] = $this->_parseDate($value); + $dates[] = $this->_parseDate(trim($value)); } else { - $dates[] = $this->_parseDateTime($value, $tzid); + $dates[] = $this->_parseDateTime(trim($value), $tzid); } } $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates); @@ -939,14 +939,54 @@ class Horde_iCalendar { case 'DCREATED': case 'LAST-MODIFIED': $value = $this->_exportDateTime($value); + break; + + + // Support additional fields after date. + case 'AALARM': + case 'DALARM': + if (isset($params['VALUE'])) { + if ($params['VALUE'] == 'DATE') { + // VCALENDAR 1.0 uses T000000 - T235959 for all day events: + if ($this->isOldFormat() && $name == 'DTEND') { + $d = new Horde_Date($value); + $value = new Horde_Date(array( + 'year' => $d->year, + 'month' => $d->month, + 'mday' => $d->mday - 1)); + $value->correct(); + $value = $this->_exportDate($value, '235959'); + } else { + $value = $this->_exportDate($value, '000000'); + } + } else { + $value = $this->_exportDateTime($value); + } + } else { + $value = $this->_exportDateTime($value); + } + + if (is_array($attribute['values']) && + count($attribute['values']) > 0) { + $values = $attribute['values']; + if ($this->isOldFormat()) { + $values = str_replace(';', '\\;', $values); + } else { + // As of rfc 2426 2.5 semicolon and comma must be + // escaped. + $values = str_replace(array('\\', ';', ','), + array('\\\\', '\\;', '\\,'), + $values); + } + $value .= ';' . implode(';', $values); + } + break; case 'DTEND': case 'DTSTART': case 'DTSTAMP': case 'DUE': - case 'AALARM': - case 'DALARM': case 'RECURRENCE-ID': case 'X-RECURRENCE-ID': if (isset($params['VALUE'])) {