diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index 4fdcfb695c..933acf834f 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -5,6 +5,7 @@ * @link http://www.egroupware.org * @package calendar * @author Ralf Becker + * @author Joerg Lehrke * @copyright (c) 2004-8 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -134,6 +135,7 @@ class calendar_bo */ protected static $cached_event = array(); protected static $cached_event_date_format = false; + protected static $cached_event_date = 0; /** * @var array $cached_holidays holidays plus birthdays (gets cached in the session for performance reasons) */ @@ -570,9 +572,17 @@ class calendar_bo $events = array(); $this->insert_all_repetitions($event,$start,$this->date2ts($this->config['horizont'],true),$events,null); - + $days = $this->so->get_recurrence_exceptions($event); + $days = is_array($days) ? $days : array(); + //error_log('set_recurrences: days' . print_r($days, true) ); foreach($events as $event) { + //error_log('set_recurrences: start = ' . $event['start'] ); + if (in_array($event['start'], $days)) + { + // we don't change the stati of recurrence exceptions + $event['participants'] = array(); + } $this->so->recurrence($event['id'],$this->date2ts($event['start'],true),$this->date2ts($event['end'],true),$event['participants']); } } @@ -657,8 +667,8 @@ class calendar_bo if ($ignore_acl || is_array($ids) || ($return = $this->check_perms(EGW_ACL_READ,$ids,0,$date_format,$date))) { if (is_array($ids) || !isset(self::$cached_event['id']) || self::$cached_event['id'] != $ids || - self::$cached_event_date_format != $date_format || - self::$cached_event['recur_type'] != MCAL_RECUR_NONE && !is_null($date) && (!$date || self::$cached_event['start'] < $date)) + self::$cached_event_date_format != $date_format || + self::$cached_event['recur_type'] != MCAL_RECUR_NONE && !is_null($date) && self::$cached_event_date != $date || (!$date || self::$cached_event['start'] < $date)) { $events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0); @@ -674,6 +684,7 @@ class calendar_bo { self::$cached_event = array_shift($events); self::$cached_event_date_format = $date_format; + self::$cached_event_date = $date; $return =& self::$cached_event; } } @@ -753,6 +764,8 @@ class calendar_bo { $search_date_ymd = (int)$this->date2string($ts); + //error_log('insert_all_repetitions search_date = ' . $search_date_ymd . ' => ' . print_r($recur_exceptions, true)); + $have_exception = !is_null($recur_exceptions) && isset($recur_exceptions[$search_date_ymd]); if (!$have_exception) // no execption by an edited event => check the deleted ones @@ -928,7 +941,7 @@ class calendar_bo { if (is_numeric($uid)) { - $info = array( + $info = array( 'res_id' => $uid, 'email' => $GLOBALS['egw']->accounts->id2name($uid,'account_email'), 'name' => trim($GLOBALS['egw']->accounts->id2name($uid,'account_firstname'). ' ' . @@ -1840,4 +1853,20 @@ class calendar_bo $GLOBALS['egw_info']['server']['webserver_url'].'/calendar/freebusy.php?user='.urlencode($user). ($pw ? '&password='.urlencode($pw) : ''); } + + /** + * Check if the event is the whole day + * + * @param event + * @return boolean true for whole day events + */ + function isWholeDay($event) + { + // check if the event is the whole day + $start = $this->date2array($event['start']); + $end = $this->date2array($event['end']); + $result = (!$start['hour'] && !$start['minute'] + && $end['hour'] == 23 && $end['minute'] == 59); + return $result; + } } diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index cf1c578c84..eba25745da 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -5,6 +5,7 @@ * @link http://www.egroupware.org * @package calendar * @author Ralf Becker + * @author Joerg Lehrke * @copyright (c) 2005-8 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -73,7 +74,7 @@ class calendar_boupdate extends calendar_bo * @param array &$event event-array, on return some values might be changed due to set defaults * @param boolean $ignore_conflicts=false just ignore conflicts or do a conflict check and return the conflicting events * @param boolean $touch_modified=true touch modificatin time and set modifing user, default true=yes - * @param boolean $ignore_acl=flase should we ignore the acl + * @param boolean $ignore_acl=false should we ignore the acl * @return mixed on success: int $cal_id > 0, on error false or array with conflicting events (only if $check_conflicts) * Please note: the events are not garantied to be readable by the user (no read grant or private)! */ @@ -256,7 +257,7 @@ class calendar_boupdate extends calendar_bo //echo "saving $event[id]="; _debug_array($event); $event2save = $event; - if (!($cal_id = $this->save($event))) + if (!($cal_id = $this->save($event, $ignore_acl))) { return $cal_id; } @@ -572,7 +573,7 @@ class calendar_boupdate extends calendar_bo break; case 'ical': - $ics = ExecMethod2('calendar.calendar_ical.exportVCal',$event['id'],'2.0',$method,false); + $ics = ExecMethod2('calendar.calendar_ical.exportVCal',$event['id'],'2.0',$method); if ($method == 'REQUEST') { $attachment = array( 'string' => $ics, @@ -666,15 +667,16 @@ class calendar_boupdate extends calendar_bo * This methode converts from user to server time and handles the insertion of users and dates of repeating events * * @param array $event + * @param boolean $ignore_acl=false should we ignore the acl * @return int/boolean $cal_id > 0 or false on error (eg. permission denied) */ - function save($event) + function save($event, $ignore_acl=false) { //error_log(__METHOD__."(".str_replace(array("\n",' '),'',print_r($event,true)).",$etag)"); // check if user has the permission to update / create the event - if ($event['id'] && !$this->check_perms(EGW_ACL_EDIT,$event['id']) || + if (!$ignore_acl && ($event['id'] && !$this->check_perms(EGW_ACL_EDIT,$event['id']) || !$event['id'] && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner']) && - !$this->check_perms(EGW_ACL_ADD,0,$event['owner'])) + !$this->check_perms(EGW_ACL_ADD,0,$event['owner']))) { return false; } @@ -705,10 +707,11 @@ class calendar_boupdate extends calendar_bo $event['alarm'][$id]['time'] = $this->date2ts($alarm['time'],true); } } - if (($cal_id = $this->so->save($event,$set_recurrences,NULL,$event['etag'])) && $set_recurrences && $event['recur_type'] != MCAL_RECUR_NONE) + $set_recurrences = false; + if (($cal_id = $this->so->save($event,$set_recurrences,0,$event['etag'])) && $set_recurrences && $event['recur_type'] != MCAL_RECUR_NONE) { $save_event['id'] = $cal_id; - $this->set_recurrences($save_event); + $this->set_recurrences($save_event, 0); } $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id,$event['id'] ? 'modify' : 'add',time()); @@ -749,13 +752,14 @@ class calendar_boupdate extends calendar_bo * @param string/int $uid account_id or 1-char type-identifer plus id (eg. c15 for addressbook entry #15) * @param int/char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A' * @param int $recur_date=0 date to change, or 0 = all since now + * @param boolean $ignore_acl=false do not check the permisions for the $uid, if true * @return int number of changed recurrences */ - function set_status($event,$uid,$status,$recur_date=0) + function set_status($event,$uid,$status,$recur_date=0, $ignore_acl=false) { $cal_id = is_array($event) ? $event['id'] : $event; //echo "

bocalupdate::set_status($cal_id,$uid,$status,$recur_date)

\n"; - if (!$cal_id || !$this->check_status_perms($uid,$event)) + if (!$cal_id || (!$ignore_acl && !$this->check_status_perms($uid,$event))) { return false; } @@ -1006,7 +1010,9 @@ class calendar_boupdate extends calendar_bo } $alarm['time'] = $this->date2ts($alarm['time'],true); // user to server-time - return $this->so->save_alarm($cal_id,$alarm); + $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id, 'modify', time()); + + return $this->so->save_alarm($cal_id,$alarm, $this->now_su); } /** @@ -1023,7 +1029,10 @@ class calendar_boupdate extends calendar_bo { return false; // no rights to delete the alarm } - return $this->so->delete_alarm($id); + + $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id, 'modify', time()); + + return $this->so->delete_alarm($id, $this->now_su); } var $categories; @@ -1046,7 +1055,7 @@ class calendar_boupdate extends calendar_bo { $cat_name = substr($cat_name, 2); } - $cat_id = $this->categories->add(array('name' => $cat_name,'descr' => $cat_name)); + $cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private')); } if ($cat_id) @@ -1086,4 +1095,95 @@ class calendar_boupdate extends calendar_bo return $cat_list; } + + /** + * 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) + */ + function find_event($event, $relax=false) + { + if ($event['uid'] && ($uidmatch = $this->read($event['uid']))) + { + if ($event['reference'] + && ($egw_event = $this->read($uidmatch['id'], $event['reference']))) + { + // Do we work with a "status only" exception here? + $match = true; + foreach (array('start','end','title','description','priority', + 'location','public','non_blocking') as $name) + { + if (isset($event[$name]) + && $event[$name] != $egw_event[$name]) + { + $match = false; + break; + } + } + if ($match) + { + //return ($uidmatch['id'] . ':' . $event['reference']); + foreach ($event['participants'] as $attendee => $status) + { + if (!isset($egw_event['participants'][$attendee]) + || $egw_event['participants'][$attendee] != $status) + { + $match = false; + break; + } + else + { + unset($egw_event['participants'][$attendee]); + } + } + if ($match && !empty($egw_event['participants'])) $match = false; + } + if ($match) return ($uidmatch['id'] . ':' . $event['reference']); + + return false; // We need to create a new "status only" exception + } + else + { + return $uidmatch['id']; + } + } + + if ($event['id'] && ($found = $this->read($event['id']))) + { + // We only do a simple consistency check + if ($found['title'] == $event['title'] + && $found['start'] == $event['start'] + && $found['end'] == $event['end']) + { + return $found['id']; + } + } + unset($event['id']); + + $query = array( + 'cal_start='.$event['start'], + 'cal_end='.$event['end'], + ); + + #foreach(array('title','location','priority','public','non_blocking','category') as $name) { + foreach (array('title','location','public','non_blocking','category') as $name) + { + if (!empty($event[$name])) $query['cal_'.$name] = $event[$name]; + } + + if($foundEvents = parent::search(array( + //'user' => $this->user, + 'query' => $query, + ))) + { + if(is_array($foundEvents)) + { + $event = array_shift($foundEvents); + return $event['id']; + } + } + return false; + } } diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index 2eb8a5a962..c1bc53e8fe 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -5,13 +5,14 @@ * @link http://www.egroupware.org * @author Lars Kneschke * @author Ralf Becker + * @author Joerg Lehrke * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package calendar * @subpackage export * @version $Id$ */ -require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/iCalendar.php'; +require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php'; /** * iCal import and export via Horde iCalendar classes @@ -47,13 +48,14 @@ class calendar_ical extends calendar_boupdate */ var $status_ical2egw = array( 'NEEDS-ACTION' => 'U', + 'NEEDS ACTION' => 'U', 'ACCEPTED' => 'A', 'DECLINED' => 'R', 'TENTATIVE' => 'T', ); /** - * @var array $status_ical2egw conversation of the priority egw => ical + * @var array $status_ical2egw conversion of the priority egw => ical */ var $priority_egw2ical = array( 0 => 0, // undefined @@ -61,14 +63,15 @@ class calendar_ical extends calendar_boupdate 2 => 5, // normal 3 => 1, // high ); + /** - * @var array $status_ical2egw conversation of the priority ical => egw + * @var array $status_ical2egw conversion of the priority ical => egw */ var $priority_ical2egw = array( 0 => 0, // undefined 9 => 1, 8 => 1, 7 => 1, 6 => 1, // low 5 => 2, // normal - 4 => 3, 2 => 3, 3 => 3, 1 => 3, // high + 4 => 3, 3 => 3, 2 => 3, 1 => 3, // high ); /** @@ -101,17 +104,60 @@ class calendar_ical extends calendar_boupdate var $productManufacturer = 'file'; var $productName = ''; + /** + * user preference: import all-day events as non blocking + * + * @var boolean + */ + var $nonBlockingAllday = false; + + /** + * user preference: attach UID entries to the DESCRIPTION + * + * @var boolean + */ + var $uidExtension = false; + + /** + * Device CTCap Properties + * + * @var array + */ + var $clientProperties; + + /** + * vCalendar Instance for parsing + * + * @var array + */ + var $vCalendar; + + /** + * Constructor + * + * @param array $_clientProperties client properties + */ + function __construct(&$_clientProperties = array()) { + parent::__construct(); + + $this->clientProperties = $_clientProperties; + $this->vCalendar = new Horde_iCalendar; + } + + /** * Exports one calendar event to an iCalendar item * * @param int/array $events (array of) cal_id or array of the events * @param string $version='1.0' could be '2.0' too * @param string $method='PUBLISH' + * @param int $recur_date=0 if set export the next recurrance at or after the timestamp, + * 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') + function &exportVCal($events, $version='1.0', $method='PUBLISH', $recur_date=0) { - // error_log(__FILE__ . __METHOD__ ."exportVCal is called "); + // error_log(__FILE__ . __METHOD__ ."exportVCal is called "); $egwSupportedFields = array( 'CLASS' => array('dbName' => 'public'), 'SUMMARY' => array('dbName' => 'title'), @@ -126,39 +172,50 @@ class calendar_ical extends calendar_boupdate 'PRIORITY' => array('dbName' => 'priority'), 'TRANSP' => array('dbName' => 'non_blocking'), 'CATEGORIES' => array('dbName' => 'category'), + 'UID' => array('dbName' => 'uid'), + 'RECURRENCE-ID' => array('dbName' => 'reference'), ); - if(!is_array($this->supportedFields)) - { - $this->setSupportedFields(); - } - if($this->productManufacturer == '' ) + if (!is_array($this->supportedFields)) $this->setSupportedFields(); + + if ($this->productManufacturer == '' ) { // syncevolution is broken - $version = "2.0"; + $version = '2.0'; } - $palm_enddate_workaround=False; - if($this->productManufacturer == 'Synthesis AG' - && strpos($this->productName, "PalmOS") ) + $servertime = false; + $date_format = 'server'; + if (strpos($this->productName, "palmos") ) { - // This workaround adds 1 day to the recur_enddate if it exists, to fix a palm bug - $palm_enddate_workaround=True; + $servertime = true; + $date_format = 'ts'; } $vcal = new Horde_iCalendar; $vcal->setAttribute('PRODID','-//eGroupWare//NONSGML eGroupWare Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'. strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang'])); - $vcal->setAttribute('VERSION',$version); - $vcal->setAttribute('METHOD',$method); + $vcal->setAttribute('VERSION', $version); + $vcal->setAttribute('METHOD', $method); if (!is_array($events)) $events = array($events); - foreach($events as $event) + while ($event = array_pop($events)) { - if (!is_array($event) && !($event = $this->read($event,null,false,'server'))) // server = timestamp in server-time(!) + if (!is_array($event) + && !($event = $this->read($event, $recur_date, false, $date_format))) { + // server = timestamp in server-time(!) return false; // no permission to read $cal_id } + if ($recur_date) + { + // force single event + foreach (array('recur_enddate','recur_interval','recur_exception','recur_data','recur_date','id','etag') as $name) + { + unset($event[$name]); + } + $event['recur_type'] = MCAL_RECUR_NONE; + } //_debug_array($event); // correct daylight saving time @@ -171,13 +228,31 @@ class calendar_ical extends calendar_boupdate $event['end'] = $event['end'] + $DSTCorrection; */ - $vevent = Horde_iCalendar::newComponent('VEVENT',$vcal); - $parameters = $attributes = array(); + if ($this->productManufacturer != 'file' + && $this->uidExtension) { + // Append UID to DESCRIPTION + if (!preg_match('/\[UID:.+\]/m', $event['description'])) { + $event['description'] .= "\n[UID:" . $event['uid'] . "]"; + } + } + + $vevent = Horde_iCalendar::newComponent('VEVENT', $vcal); + $parameters = $attributes = $values = array(); + + if ($this->productManufacturer == 'sonyericsson') + { + $eventDST = date('I', $event['start']); + if ($eventDST) + { + $attributes['X-SONYERICSSON-DST'] = 4; + } + } foreach($egwSupportedFields as $icalFieldName => $egwFieldInfo) { - if($this->supportedFields[$egwFieldInfo['dbName']]) + if ($this->supportedFields[$egwFieldInfo['dbName']]) { + $values[$icalFieldName] = array(); switch($icalFieldName) { case 'ATTENDEE': @@ -185,6 +260,7 @@ class calendar_ical extends calendar_boupdate foreach((array)$event['participants'] as $uid => $status) { if (!($info = $this->resource_info($uid))) continue; + if ($uid == $event['owner']) continue; // Organizer // RB: MAILTO href contains only the email-address, NO cn! $attributes['ATTENDEE'][] = $info['email'] ? 'MAILTO:'.$info['email'] : ''; // ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT} NOT used by eGW atm. @@ -221,53 +297,40 @@ class calendar_ical extends calendar_boupdate } break; - case 'CLASS': - $attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'PRIVATE'; - break; + case 'CLASS': + $attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'CONFIDENTIAL'; + break; case 'ORGANIZER': // according to iCalendar standard, ORGANIZER not used for events in the own calendar //if ($event['owner'] != $this->user) - if (!isset($event['participants'][$event['owner']]) || count($event['participants']) > 1) - { + //if (!isset($event['participants'][$event['owner']]) || count($event['participants']) > 1) + //{ $mailtoOrganizer = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_email'); $attributes['ORGANIZER'] = $mailtoOrganizer ? 'MAILTO:'.$mailtoOrganizer : ''; $parameters['ORGANIZER']['CN'] = '"'.trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname').' '. $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')).'"'; - } + //} + break; + + case 'DTSTART': + if ($servertime) + { + $attributes['DTSTART'] = date('Ymd\THis', $event['start']); + } + else + { + $attributes['DTSTART'] = $event['start']; + } break; case 'DTEND': // write start + end of whole day events as dates - // $event['end'] += 12*3600; - // we need the date of the next day, as DTEND is non-inclusive (= exclusive) in rfc2445 - //njv: This appears to be another urban myth misinterpretation 20090220 - - /* - DTSTART;VALUE=DATE:20090317 implies 000000Z - DTEND;VALUE=DATE:20090317 implies 235959Z - And DEFINES an ALLDAY event - and as such is "inclusive" - some clients(Lightning, Korganiser?) use "exclusive" - Legacy: EGW held 235900 As DTEND allday, such entries in databases will exist - EGW is internally correct on 235959 "inclusive" - */ - // Whats in DTSTART/DTEND - //error_log(__FILE__ . __METHOD__. "\nDTSTART:". print_r($event['start'],true)."\nDTEND:".print_r($event['end'],true)); - //Lets do it right first: - //If (YMD DTSTART == YMD DTEND) and (His DTSTART == 00:00:00 )and (His DTEND in 235900 or 235959) then we are oneday allday return YMD DTSTART DTEND - //if (YMD DTSTART <= YMD DTEND) and (HIS DTSTART == 00:00:00) (His DTEND in 235900 or 235959) then we are all day or allday multiday ending in an allday return DTG DTSTART and DTEND - //error_log(__FILE__ . __METHOD__ ." :$this->productManufacturer :$this->productName" ); - // returns "GroupDAV" "kde" - - if (date('H:i:s',$event['start']) == '00:00:00' && in_array(date('H:i:s',$event['end']),array('23:59:59','23:59:00'))) + if ($this->isWholeDay($event)) { - //if kde or lightning increase dtend by 24 hours? 86400 secs - if($this->productName == "kde" || $this->productName == "lightning"){ - $event['end'] += 86400; - } - foreach(array('start' => 'DTSTART','end' => 'DTEND') as $f => $t) + $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 = calendar_bo::date2array($event[$f]); + $arr = $this->date2array($event[$f]); $vevent->setAttribute($t, array('year' => $arr['year'],'month' => $arr['month'],'mday' => $arr['day']), array('VALUE' => 'DATE')); } @@ -275,7 +338,14 @@ class calendar_ical extends calendar_boupdate } else { - $attributes['DTEND'] = $event['end']; + if ($servertime) + { + $attributes['DTEND'] = date('Ymd\THis', $event['end']); + } + else + { + $attributes['DTEND'] = $event['end']; + } } break; @@ -309,12 +379,15 @@ class calendar_ical extends calendar_boupdate if ($event['recur_enddate']) { $recur_enddate = (int)$event['recur_enddate']; - if ($palm_enddate_workaround) + $recur_enddate += 24 * 60 * 60 - 1; + if ($servertime) { - $recur_enddate += 86400; + $rrule['UNTIL'] = date('Ymd\THis', $recur_enddate); + } + else + { + $rrule['UNTIL'] = $vcal->_exportDateTime($recur_enddate); } - # append T and the Endtime, since the RRULE seems not to be understood by the client without it - $rrule['UNTIL'] = date('Ymd',$recur_enddate).'T'.date('His',($event['end']?$event['end']:$event['start'])) ; } else { @@ -344,9 +417,31 @@ class calendar_ical extends calendar_boupdate strtoupper(substr(date('l',$event['start']),0,2)); break; } - if ($event['recur_interval'] > 1) $rrule['INTERVAL'] = $event['recur_interval']; - if ($event['recur_enddate']) $rrule['UNTIL'] = date('Ymd',$event['recur_enddate']); // only day is set in eGW - + if ($event['recur_interval'] > 1) + { + $rrule['INTERVAL'] = $event['recur_interval']; + } + if ($event['recur_enddate']) + { + // We use end of day in vCal + $recur_enddate = (int)$event['recur_enddate']; + $recur_enddate += 24 * 60 * 60 - 1; + if ($this->isWholeDay($event)) + { + $rrule['UNTIL'] = date('Ymd', $recur_enddate); + } + else + { + if ($servertime) + { + $rrule['UNTIL'] = date('Ymd\THis', $recur_enddate); + } + else + { + $rrule['UNTIL'] = $vcal->_exportDateTime($recur_enddate); + } + } + } // no idea how to get the Horde parser to produce a standard conformant // RRULE:FREQ=... (note the double colon after RRULE, we cant use the $parameter array) // so we create one value manual ;-) @@ -359,15 +454,69 @@ class calendar_ical extends calendar_boupdate break; case 'EXDATE': - if ($event['recur_exception']) + if ($event['recur_type'] == MCAL_RECUR_NONE) break; + $days = array(); + $participants = $this->so->get_participants($event['id'], 0); + + // Check if the stati for all participants are identical for all recurrences + foreach ($participants as $uid => $attendee) { - $days = array(); - foreach($event['recur_exception'] as $day) + switch ($attendee['type']) { - $days[] = date('Ymd',$day); + case 'u': // account + case 'c': // contact + case 'e': // email address + $recurrences = $this->so->get_recurrences($event['id'], $uid); + foreach ($recurrences as $rdate => $recur_status) + { + if ($rdate && $recur_status != $recurrences[0]) + { + // Every distinct status results in an exception + $days[] = $rdate; + } + } + break; + default: // We don't handle the rest + break; } - $attributes['EXDATE'] = implode(',',$days); - $parameters['EXDATE']['VALUE'] = 'DATE'; + } + if (is_array($event['recur_exception'])) + { + $days = $days + $event['recur_exception']; + } + if (!empty($days)) + { + $days = array_unique($days); + sort($days); + // use 'DATE' instead of 'DATE-TIME' on whole day events + if ($this->isWholeDay($event)) + { + $value_type = 'DATE'; + foreach($days as $id => $timestamp) + { + $arr = $this->date2array($timestamp); + $days[$id] = array( + 'year' => $arr['year'], + 'month' => $arr['month'], + 'mday' => $arr['day'], + ); + } + } + else + { + $value_type = 'DATE-TIME'; + if ($servertime) + { + foreach($days as $id => $timestamp) + { + $days[$id] = date('Ymd\THis', $timestamp); + + } + } + } + $attributes['EXDATE'] = ''; + $values['EXDATE'] = $days; + $parameters['EXDATE']['VALUE'] = $value_type; } break; @@ -377,30 +526,104 @@ class calendar_ical extends calendar_boupdate case 'TRANSP': if ($version == '1.0') { - $attributes['TRANSP'] = $event['non_blocking'] ? 1 : 0; + $attributes['TRANSP'] = ($event['non_blocking'] ? 1 : 0); } else { - $attributes['TRANSP'] = $event['non_blocking'] ? 'TRANSPARENT' : 'OPAQUE'; + $attributes['TRANSP'] = ($event['non_blocking'] ? 'TRANSPARENT' : 'OPAQUE'); } break; case 'CATEGORIES': if ($event['category']) { - $attributes['CATEGORIES'] = implode(',',$this->get_categories($event['category'])); + $attributes['CATEGORIES'] = ''; + $values['CATEGORIES'] = $this->get_categories($event['category']); + } + break; + + case 'RECURRENCE-ID': + if ($recur_date) + { + // We handle a status only exception + if ($this->isWholeDay($event)) + { + $arr = $this->date2array($recur_date); + $vevent->setAttribute('RECURRENCE-ID', array( + 'year' => $arr['year'], + 'month' => $arr['month'], + 'mday' => $arr['day']), + array('VALUE' => 'DATE') + ); + } + else + { + if ($servertime) + { + $attributes['RECURRENCE-ID'] = date('Ymd\THis', $recur_date); + } + else + { + $attributes['RECURRENCE-ID'] = $recur_date; + } + } + } + elseif ($event['reference']) + { + if ($this->isWholeDay($event)) + { + $arr = $this->date2array($event['reference']); + $vevent->setAttribute('RECURRENCE-ID', array( + 'year' => $arr['year'], + 'month' => $arr['month'], + 'mday' => $arr['day']), + array('VALUE' => 'DATE') + ); + } + else + { + if ($servertime) + { + $attributes['RECURRENCE-ID'] = date('Ymd\THis', $event['reference']); + } + else + { + $attributes['RECURRENCE-ID'] = $event['reference']; + } + } } break; default: - if ($event[$egwFieldInfo['dbName']]) // dont write empty fields - { - $attributes[$icalFieldName] = $event[$egwFieldInfo['dbName']]; + if (isset($this->clientProperties[$icalFieldName]['Size'])) { + $size = $this->clientProperties[$icalFieldName]['Size']; + $noTruncate = $this->clientProperties[$icalFieldName]['NoTruncate']; + #Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " . + # ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); + } else { + $size = -1; + $noTruncate = false; } - break; + $value = $event[$egwFieldInfo['dbName']]; + $cursize = strlen($value); + if (($size > 0) && $cursize > $size) { + if ($noTruncate) { + Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size", + __FILE__, __LINE__, PEAR_LOG_WARNING); + continue; // skip field + } + // truncate the value to size + $value = substr($value, 0, $size - 1); + Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size", + __FILE__, __LINE__, PEAR_LOG_INFO); + } + if (!empty($value) || ($size >= 0 && !$noTruncate)) { + $attributes[$icalFieldName] = $value; + } + break; } } } - if(strtolower($this->productManufacturer) == 'nokia') { + if($this->productManufacturer == 'nokia') { if($event['special'] == '1') { $attributes['X-EPOCAGENDAENTRYTYPE'] = 'ANNIVERSARY'; $attributes['DTEND'] = $attributes['DTSTART']; @@ -415,701 +638,492 @@ class calendar_ical extends calendar_boupdate $modified = $GLOBALS['egw']->contenthistory->getTSforAction('calendar',$event['id'],'modify'); $created = $GLOBALS['egw']->contenthistory->getTSforAction('calendar',$event['id'],'add'); if (!$created && !$modified) $created = $event['modified']; - if ($created) $attributes['CREATED'] = $created; - if (!$modified) $modified = $event['modified']; - if ($modified) $attributes['LAST-MODIFIED'] = $modified; - - foreach($event['alarm'] as $alarmID => $alarmData) + if ($created) { - if ($version == '1.0') + if ($servertime) { - $attributes['DALARM'] = $vcal->_exportDateTime($alarmData['time']); - $attributes['AALARM'] = $vcal->_exportDateTime($alarmData['time']); - // lets take only the first alarm - break; + $attributes['CREATED'] = date('Ymd\THis', $created); } else { + $attributes['CREATED'] = $created; + } + } + if (!$modified) $modified = $event['modified']; + if ($modified) + { + if ($servertime) + { + $attributes['LAST-MODIFIED'] = date('Ymd\THis', $modified); + } + else + { + $attributes['LAST-MODIFIED'] = $modified; + } + } + if ($servertime) + { + $attributes['DTSTAMP'] = date('Ymd\THis', time()); + } + else + { + $attributes['DTSTAMP'] = time(); + } + foreach($event['alarm'] as $alarmID => $alarmData) { + if ($version == '1.0') { + if ($servertime) + { + $attributes['DALARM'] = date('Ymd\THis', $alarmData['time']); + $attributes['AALARM'] = date('Ymd\THis', $alarmData['time']); + } + else + { + $attributes['DALARM'] = $alarmData['time']; + $attributes['AALARM'] = $alarmData['time']; + } + // lets take only the first alarm + break; + } else { // 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']) - { + if (!$alarmData['offset'] && !$alarmData['time']) { error_log("Couldn't add VALARM (no alarm time info)"); continue; } // RFC requires DESCRIPTION for DISPLAY - if (!$event['title'] && !$event['description']) - { + if (!$event['title'] && !$description) { error_log("Couldn't add VALARM (no description)"); continue; } $valarm = Horde_iCalendar::newComponent('VALARM',$vevent); - if ($alarmData['offset']) - { + if ($alarmData['offset']) { $valarm->setAttribute('TRIGGER', -$alarmData['offset'], array('VALUE' => 'DURATION', 'RELATED' => 'START')); - } - else - { - $valarm->setAttribute('TRIGGER', $alarmData['time'], - array('VALUE' => 'DATE-TIME')); + } else { + if ($servertime) + { + $value = date('Ymd\THis', $alarmData['time']); + } + else + { + $value = $alarmData['time']; + } + $valarm->setAttribute('TRIGGER', $value, array('VALUE' => 'DATE-TIME')); } $valarm->setAttribute('ACTION','DISPLAY'); - $valarm->setAttribute('DESCRIPTION',$event['title'] ? $event['title'] : $event['description']); + $valarm->setAttribute('DESCRIPTION',$event['title'] ? $event['title'] : $description); $vevent->addComponent($valarm); } } - $attributes['UID'] = $event['uid']; - foreach($attributes as $key => $value) - { - foreach(is_array($value)&&$parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData) - { + foreach($attributes as $key => $value) { + 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) ? $parameters[$key][$valueID] : $parameters[$key], - $GLOBALS['egw']->translation->charset(),'UTF-8'); - $vevent->setAttribute($key, $valueData, $paramData); + $paramData = (array) $GLOBALS['egw']->translation->convert(is_array($value) ? + $parameters[$key][$valueID] : $parameters[$key], + $GLOBALS['egw']->translation->charset(),'UTF-8'); + $valuesData = (array) $GLOBALS['egw']->translation->convert($values[$key], + $GLOBALS['egw']->translation->charset(),'UTF-8'); + //echo "$key:$valueID: value=$valueData, param=".print_r($paramDate,true)."\n"; + $vevent->setAttribute($key, $valueData, $paramData, true, $valuesData); $options = array(); if ($paramData['CN']) $valueData .= $paramData['CN']; // attendees or organizer CN can contain utf-8 content - - if($this->productManufacturer == 'file' || $this->productManufacturer == 'GroupDAV') { - } - else - { - if($key != 'RRULE' && preg_match('/([\000-\012\015\016\020-\037\075])/',$valueData)) - { - error_log(__FILE__ . __METHOD__ . "QP key $key: ".print_r($valueData,true)); - $options['ENCODING'] = 'QUOTED-PRINTABLE'; - } - } - - if($this->productManufacturer != 'GroupDAV' && preg_match('/([\177-\377])/',$valueData)) + /*if($key != 'RRULE' && preg_match('/([\000-\012\015\016\020-\037\075])/',$valueData)) { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + }*/ + if ($this->productManufacturer != 'groupdav' && preg_match('/([\177-\377])/', $valueData)) { $options['CHARSET'] = 'UTF-8'; } - if(preg_match('/([\000-\012])/',$valueData)) + if (preg_match('/([\000-\012])/', $valueData)) { - error_log(__FILE__ . __METHOD__ ."Has invalid XML data :$valueData"); + error_log(__FILE__ . __METHOD__ ."Has invalid XML data :$valueData"); } - $vevent->setParameter($key, $options); + $vevent->setParameter($key, $options); } } $vcal->addComponent($vevent); } + //_debug_array($vcal->exportvCalendar()); + + $retval = $vcal->exportvCalendar(); + Horde::logMessage("exportVCAL:\n" . print_r($retval, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $retval; - return $vcal->exportvCalendar(); } /** * Import an iCal * * @param string $_vcalData - * @param int $cal_id=-1 must be -1 for new entrys! + * @param int $cal_id=-1 must be -1 for new entries! * @param string $etag=null if an etag is given, it has to match the current etag or the import will fail + * @param boolean $merge=false merge data with existing entry + * @param int $recur_date=0 if set, import the recurrance at this timestamp, + * default 0 => import whole series (or events, if not recurring) * @return int|boolean cal_id > 0 on success, false on failure or 0 for a failed etag */ - function importVCal($_vcalData, $cal_id=-1,$etag=null) + function importVCal($_vcalData, $cal_id=-1, $etag=null, $merge=false, $recur_date=0) { - // our (patched) horde classes, do NOT unfold folded lines, which causes a lot trouble in the import - $_vcalData = preg_replace("/[\r\n]+ /",'',$_vcalData); + $Ok = false; // returning false, if file contains no components + $vcal = new Horde_iCalendar; - if(!$vcal->parsevCalendar($_vcalData)) + if (!$vcal->parsevCalendar($_vcalData)) { - return FALSE; + return $Ok; + } + + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } + else + { + $minimum_uid_length = 8; } $version = $vcal->getAttribute('VERSION'); - error_log(__FILE__ . __METHOD__ ."Called with :\n ". print_r($_vcalData,true)); - if(!is_array($this->supportedFields)) - { - $this->setSupportedFields(); - } - //echo "supportedFields="; _debug_array($this->supportedFields); - $syncevo_enddate_fix = False; - if( $this->productManufacturer == '' && $this->productName == '' ) - { - // syncevolution needs an adjusted recur_enddate - $syncevo_enddate_fix = True; - } + if (!is_array($this->supportedFields)) $this->setSupportedFields(); - $Ok = false; // returning false, if file contains no components - foreach($vcal->getComponents() as $component) + foreach ($vcal->getComponents() as $component) { - if(is_a($component, 'Horde_iCalendar_vevent')) + if (is_a($component, 'Horde_iCalendar_vevent')) { - $supportedFields = $this->supportedFields; - #$event = array('participants' => array()); - $event = array(); - $alarms = array(); - $vcardData = array( - 'recur_type' => MCAL_RECUR_NONE, - 'recur_exception' => array(), - ); - // lets see what we can get from the vcard - foreach($component->_attributes as $attributes) - { - //error_log(__FILE__ . __METHOD__ .":" . print_r($attributes,true)); - switch($attributes['name']) - { - case 'AALARM': - case 'DALARM': - if (preg_match('/.*Z$/',$attributes['value'],$matches)) { - $alarmTime = $vcal->_parseDateTime($attributes['value']); - $alarms[$alarmTime] = array( - 'time' => $alarmTime - ); - } elseif (preg_match('/(........T......);;(\d*);$/',$attributes['value'],$matches)) { - //error_log(print_r($matches,true)); - $alarmTime = $vcal->_parseDateTime($matches[1]); - $alarms[$alarmTime] = array( - 'time' => $alarmTime - ); - } elseif (preg_match('/(........T......Z);;(\d*);$/',$attributes['value'],$matches)) { - //error_log(print_r($matches,true)); - $alarmTime = $vcal->_parseDateTime($matches[1]); - $alarms[$alarmTime] = array( - 'time' => $alarmTime - ); - } elseif (preg_match('/(........T......)$/',$attributes['value'],$matches)) { - $alarmTime = $vcal->_parseDateTime($attributes['value']); - $alarms[$alarmTime] = array( - 'time' => $alarmTime - ); - } - break; - case 'CLASS': - $vcardData['public'] = (int)(strtolower($attributes['value']) == 'public'); - break; - case 'DESCRIPTION': - $vcardData['description'] = $attributes['value']; - break; - case 'DTEND': - /* - DTSTART;VALUE=DATE:20090317 implies 000000Z - DTEND;VALUE=DATE:20090317 implies 235959Z - And DEFINES an ALLDAY event - and as such is "inclusive" - some clients(Lightning, Korganiser?) use "exclusive" - Legacy: EGW held 235900 As DTEND allday, such entries in databases will exist - EGW is internally correct on 235959 "inclusive" - */ - // We aren't allday - $allday = false; - // get and ensure the DTSTART/DTEND is a numeric Timestamp - $dtstart_ts = self::_get_attribute($component->_attributes,'DTSTART'); - $dtstart_ts = is_numeric($dtstart_ts) ? $dtstart_ts : $this->date2ts($dtstart_ts); - $dtend_ts = self::_get_attribute($component->_attributes,'DTEND'); - $dtend_ts = is_numeric($dtend_ts) ? $dtend_ts : $this->date2ts($dtend_ts); - //Plausibility Check and Convert Vcal/ical Allday to EGW Allday - if($dtstart_ts <= $dtend_ts && date('H:i:s',$dtend_ts) == '00:00:00'){ - // We are a one day Allday event or Allday last day of a sequence - // dtend_ts += 23:59:59 +=86339 = EGW intern - $allday = true; - $dtend_ts += 86399; - } - //if kde or lightning reduce dtend by 24 hours? 86400 secs - if($allday && ($this->productName == "kde" || $this->productName == "lightning")){ - $dtend_ts -= 86400; - } - $vcardData['end'] = $dtend_ts; - break; - case 'DTSTART': - $vcardData['start'] = $attributes['value']; - break; - case 'LOCATION': - $vcardData['location'] = $attributes['value']; - break; - case 'RRULE': - $recurence = $attributes['value']; - $type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0]; - // vCard 2.0 values for all types - if (preg_match('/UNTIL=([0-9T]+)/',$recurence,$matches)) - { - $vcardData['recur_enddate'] = $vcal->_parseDateTime($matches[1]); - } - elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches)) - { - $vcardData['recur_count'] = (int)$matches[1]; - } - if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches)) - { - // 1 is invalid,, egw uses 0 for interval - $vcardData['recur_interval'] = (int) $matches[1] != 0 ? (int) $matches[1] : 0; - } - if (!isset($vcardData['start'])) // it might not yet be set, because the RRULE is before it - { - $vcardData['start'] = self::_get_attribute($component->_attributes,'DTSTART'); - $vcardData['end'] = self::_get_attribute($component->_attributes,'DTEND'); - } - $vcardData['recur_data'] = 0; - switch($type) - { - case 'W': - case 'WEEKLY': - $days = array(); - if(preg_match('/W(\d+) (.*) (.*)/',$recurence, $recurenceMatches)) // 1.0 - { - $vcardData['recur_interval'] = $recurenceMatches[1]; - $days = explode(' ',trim($recurenceMatches[2])); - if($recurenceMatches[3] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[3]); - $recur_days = $this->recur_days_1_0; - } - elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0 - { - $days = explode(',',$recurenceMatches[1]); - $recur_days = $this->recur_days; - } - else // no day given, use the day of dtstart - { - $vcardData['recur_data'] |= 1 << (int)date('w',$vcardData['start']); - $vcardData['recur_type'] = MCAL_RECUR_WEEKLY; - } - if ($days) - { - foreach($recur_days as $id => $day) - { - if (in_array(strtoupper(substr($day,0,2)),$days)) - { - $vcardData['recur_data'] |= $id; - } - } - $vcardData['recur_type'] = MCAL_RECUR_WEEKLY; - } + $event = $this->vevent2egw($component, $version, $this->supportedFields); - if (!empty($vcardData['recur_count'])) - { - $vcardData['recur_enddate'] = mktime(0,0,0, - date('m',$vcardData['start']), - date('d',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1)*7), - date('Y',$vcardData['start'])); - } - break; - - case 'D': // 1.0 - if(preg_match('/D(\d+) #(.\d)/', $recurence, $recurenceMatches)) { - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] > 0 && $vcardData['end']) { - $vcardData['recur_enddate'] = mktime( - date('H', $vcardData['end']), - date('i', $vcardData['end']), - date('s', $vcardData['end']), - date('m', $vcardData['end']), - date('d', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']), - date('Y', $vcardData['end']) - ); - } - } elseif(preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches)) { - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] != '#0') { - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[2]); - } - } else { - break; - } - // fall-through - case 'DAILY': // 2.0 - $vcardData['recur_type'] = MCAL_RECUR_DAILY; - - if (!empty($vcardData['recur_count'])) - { - $vcardData['recur_enddate'] = mktime(0,0,0, - date('m',$vcardData['start']), - date('d',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1)), - date('Y',$vcardData['start'])); - } - break; - - case 'M': - if(preg_match('/MD(\d+) #(.\d)/', $recurence, $recurenceMatches)) { - $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY; - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] > 0 && $vcardData['end']) { - $vcardData['recur_enddate'] = mktime( - date('H', $vcardData['end']), - date('i', $vcardData['end']), - date('s', $vcardData['end']), - date('m', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']), - date('d', $vcardData['end']), - date('Y', $vcardData['end']) - ); - } - } elseif(preg_match('/MD(\d+) (.*)/',$recurence, $recurenceMatches)) { - $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY; - if($recurenceMatches[1] > 1) - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[2]); - } elseif(preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches)) { - $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_WDAY; - if($recurenceMatches[1] > 1) - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[4] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[4]); - } - break; - case 'MONTHLY': - $vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ? - MCAL_RECUR_MONTHLY_WDAY : MCAL_RECUR_MONTHLY_MDAY; - - if (!empty($vcardData['recur_count'])) - { - $vcardData['recur_enddate'] = mktime(0,0,0, - date('m',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1)), - date('d',$vcardData['start']), - date('Y',$vcardData['start'])); - } - break; - - case 'Y': // 1.0 - if(preg_match('/YM(\d+) #(.\d)/', $recurence, $recurenceMatches)) { - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] > 0 && $vcardData['end']) { - $vcardData['recur_enddate'] = mktime( - date('H', $vcardData['end']), - date('i', $vcardData['end']), - date('s', $vcardData['end']), - date('m', $vcardData['end']), - date('d', $vcardData['end']), - date('Y', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']) - ); - } - } elseif(preg_match('/YM(\d+) (.*)/',$recurence, $recurenceMatches)) { - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] != '#0') { - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[2]); - } - } else { - break; - } - // fall-through - case 'YEARLY': // 2.0 - $vcardData['recur_type'] = MCAL_RECUR_YEARLY; - - if (!empty($vcardData['recur_count'])) - { - $vcardData['recur_enddate'] = mktime(0,0,0, - date('m',$vcardData['start']), - date('d',$vcardData['start']), - date('Y',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1))); - } - break; - } - if( $syncevo_enddate_fix && $vcardData['recur_enddate'] ) - { - // Does syncevolution need to adjust recur_enddate - $vcardData['recur_enddate'] = (int)$vcardData['recur_enddate'] + 86400; - } - break; - case 'EXDATE': - $vcardData['recur_exception'] = array_merge($vcardData['recur_exception'],$attributes['value']); - break; - case 'SUMMARY': - $vcardData['title'] = $attributes['value']; - break; - case 'UID': - $event['uid'] = $vcardData['uid'] = $attributes['value']; - if ($cal_id <= 0 && !empty($vcardData['uid']) && ($uid_event = $this->read($vcardData['uid']))) - { - $cal_id = $event['id'] = $uid_event['id']; - unset($uid_event); - } - break; - case 'TRANSP': - if($version == '1.0') { - $vcardData['non_blocking'] = $attributes['value'] == 1; - } else { - $vcardData['non_blocking'] = $attributes['value'] == 'TRANSPARENT'; - } - break; - case 'PRIORITY': - $vcardData['priority'] = (int) $this->priority_ical2egw[$attributes['value']]; - break; - case 'CATEGORIES': - if ($attributes['value']) - { - $vcardData['category'] = $this->find_or_add_categories(explode(',',$attributes['value'])); - } - else - { - $vcardData['category'] = array(); - } - break; - case 'ATTENDEE': - error_log(__FILE__ . __METHOD__ . "case:ATTENDEE:" . print_r($attributes,true)); - if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) || - preg_match('/<([@.a-z0-9_-]+)>/i',$attributes['value'],$matches)) - { - $email = $matches[1]; - } - elseif(strpos($attributes['value'],'@') !== false) - { - $email = $attributes['value']; - } - else - { - $email = false; // no email given - } - $searcharray = array(); - if ($email) $searcharray = array('email' => $email, 'email_home' => $email); - if (isset($attributes['params']['CN']) && $attributes['params']['CN']) - { - if($attributes['params']['CN'][0] == '"' && substr($attributes['params']['CN'],-1) == '"') - { - $attributes['params']['CN'] = substr($attributes['params']['CN'],1,-1); - } - $searcharray['n_fn'] = $attributes['params']['CN']; - } - if (($uid = $attributes['params']['X-EGROUPWARE-UID']) && - ($info = $this->resource_info($uid)) && (!$email || $info['email'] == $email)) - { - // we use the (checked) X-EGROUPWARE-UID - } - /*elseif($attributes['params']['CUTYPE'] == 'RESOURCE') - { - - }*/ - elseif($attributes['value'] == 'Unknown') - { - $uid = $GLOBALS['egw_info']['user']['account_id']; - } - elseif ($email && ($uid = $GLOBALS['egw']->accounts->name2id($email,'account_email'))) - { - // we use the account we found - } - elseif(!$searcharray) - { - continue; // participants without email AND CN --> ignore it - } - elseif ((list($data) = ExecMethod2('addressbook.addressbook_bo.search',$searcharray, - array('id','egw_addressbook.account_id as account_id','n_fn'),'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC','','',false,'OR'))) - { - $uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id']; - } - else - { - if (!$email) $email = 'no-email@egroupware.org'; // set dummy email to store the CN - $uid = 'e'.($attributes['params']['CN'] ? $attributes['params']['CN'].' <'.$email.'>' : $email); - } - - error_log(__FILE__ . __METHOD__ . "\n ATTENDEE:" . print_r($attributes['value'],true)); - $event['participants'][$uid] = isset($attributes['params']['PARTSTAT']) ? - $this->status_ical2egw[strtoupper($attributes['params']['PARTSTAT'])] : - ($uid == $event['owner'] ? 'A' : 'U'); - - break; - case 'ORGANIZER': // will be written direct to the event - error_log(__FILE__ . __METHOD__ . "\n ORGANIZER:" . print_r($attributes['value'],true)); - if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) && - ($uid = $GLOBALS['egw']->accounts->name2id($matches[1],'account_email'))) - { - $event['owner'] = $uid; - } - break; - case 'CREATED': // will be written direct to the event - if ($event['modified']) break; - // fall through - case 'LAST-MODIFIED': // will be written direct to the event - $event['modified'] = $attributes['value']; - break; - } - } - //error_log(__FILE__ . __METHOD__ . "\n after case:" . print_r($event['participants'],true)); - // check if the entry is a birthday - // this field is only set from NOKIA clients - $agendaEntryType = $component->getAttribute('X-EPOCAGENDAENTRYTYPE'); - if (!is_a($agendaEntryType, 'PEAR_Error')) { - if(strtolower($agendaEntryType) == 'anniversary') { - $event['special'] = '1'; - $event['non_blocking'] = '1'; - // make it a whole day event for eGW - $vcardData['end'] = $vcardData['start'] + 86399; - } - elseif(strtolower($agendaEntryType) == 'event') { - $event['special'] = '2'; - $event['non_blocking'] = '1'; - } - } - - if(!empty($vcardData['recur_enddate'])) - { - // reset recure_enddate to 00:00:00 on the last day - $vcardData['recur_enddate'] = mktime(0, 0, 0, - date('m',$vcardData['recur_enddate']), - date('d',$vcardData['recur_enddate']), - date('Y',$vcardData['recur_enddate']) - ); - } - //echo "event=";_debug_array($vcardData); - - // now that we know what the vard provides, we merge that data with the information we have about the device - $event['priority'] = 2; - if($cal_id > 0) - { + if ($cal_id > 0) { $event['id'] = $cal_id; } - while(($fieldName = array_shift($supportedFields))) + + if ($this->productManufacturer == '' && $this->productName == '' + && !empty($event['recur_enddate'])) { - switch($fieldName) - { - case 'alarms': - // not handled here - break; - case 'recur_type': - $event['recur_type'] = $vcardData['recur_type']; - if ($event['recur_type'] != MCAL_RECUR_NONE) - { - foreach(array('recur_interval','recur_enddate','recur_data','recur_exception') as $r) - { - if(isset($vcardData[$r])) - { - $event[$r] = $vcardData[$r]; - } - } - } - unset($supportedFields['recur_type']); - unset($supportedFields['recur_interval']); - unset($supportedFields['recur_enddate']); - unset($supportedFields['recur_data']); - break; - default: - if (isset($vcardData[$fieldName])) - { - $event[$fieldName] = $vcardData[$fieldName]; - } - unset($supportedFields[$fieldName]); - break; - } + // syncevolution needs an adjusted recur_enddate + $event['recur_enddate'] = (int)$event['recur_enddate'] + 86400; } - // add ourself to new events as participant - if ($cal_id == -1 && (!isset($this->supportedFields['participants']) || + if ($cal_id < 0 && (!isset($this->supportedFields['participants']) || !isset($event['participants'][$GLOBALS['egw_info']['user']['account_id']]))) { + // add ourself to new events as participant $event['participants'][$GLOBALS['egw_info']['user']['account_id']] = 'A'; } - // If this is an updated meeting, and the client doesn't support - // participants OR the event no longer contains participants, add them back - if( $cal_id > 0 && (!isset($this->supportedFields['participants']) || !count($event['participants']))) - { - - if (($egw_event = $this->read($cal_id))) - { - $event['participants'] = $egw_event['participants']; - $event['participant_types'] = $egw_event['participant_types']; - } - } + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + // No RECCURENCE-ID for series events + $event['reference'] = 0; + } - // Check for resources, and don't remove them - if( $cal_id > 0 ) + if ($cal_id > 0 && ($egw_event = $this->read($cal_id, $recur_date))) { - // for each existing participant: - if (($egw_event = $this->read($cal_id))) + // overwrite with server data for merge + if ($merge) { - foreach( $egw_event['participants'] as $uid => $status ) + if ($egw_event['recur_type'] != MCAL_RECUR_NONE && $recur_date) { - // Is it a resource and not longer present in the event? + // update only the stati of the exception + if ($this->check_perms(EGW_ACL_EDIT, $cal_id)) + { + $this->update_status($event, $egw_event, $recur_date); + } + $Ok = $cal_id . ':' . $recur_date; + continue; // nothing more to do + } + foreach ($egw_event as $key => $value) + { + switch ($key) + { + case 'participants_types': + continue; + + case 'participants': + foreach ($egw_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 // not 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'] = $egw_event['participants']; + $event['participant_types'] = $egw_event['participant_types']; + } + + foreach ($egw_event['participants'] as $uid => $status) + { + // Is it a resource and no longer present in the event? if ( $uid[0] == 'r' && !isset($event['participants'][$uid]) ) { // Add it back in $event['participants'][$uid] = $event['participant_types']['r'][substr($uid,1)] = $status; } } + // avoid that iCal changes the organizer, which is not allowed + $event['owner'] = $egw_event['owner']; + } + } + else + { + // new event + $cal_id = -1; + $recur_date = $event['reference']; + } + + if (empty($event['uid']) && $cal_id > 0 && + ($egw_event = $this->read($cal_id))) + { + $event['uid'] = $egw_event['uid']; + } + + if ($event['recur_type'] == MCAL_RECUR_NONE + && !empty($event['uid']) && $recur_date) + { + // We handle a recurrence exception + $recur_exceptions = $this->so->get_related($event['uid']); + $recur_id = array_search($recur_date, $recur_exceptions); + if ($recur_id === false || !($egw_event = $this->read($recur_id))) + { + // We found no real exception, let't try "status only" + if (($egw_event = $this->read($event['uid'])) + && $egw_event['recur_type'] != MCAL_RECUR_NONE) + { + $unchanged = true; + foreach (array('uid','owner','title','description', + 'location','priority','public','special','non_blocking') as $key) + { + //Horde::logMessage('importVCAL test ' .$key . ': '. $egw_event[$key] . ' == ' .$event[$key], + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + if (!empty($event[$key]) //|| !empty($egw_event[$key])) + && $egw_event[$key] != $event[$key]) + { + $unchanged = false; + break; + } + } + if ($unchanged) + { + Horde::logMessage('importVCAL event unchanged', + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + // We can handle this without an exception entry + $recur_exceptions = array(); + foreach ($egw_event['recur_exception'] as $recur_exception) + { + //Horde::logMessage('importVCAL exception ' .$recur_exception, + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($recur_exception != $recur_date) + { + $recur_exceptions[] = $recur_exception; + } + } + //Horde::logMessage("importVCAL exceptions\n" . print_r($recur_exceptions, true), + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + $egw_event['recur_exception'] = $recur_exceptions; + $this->update($egw_event, true); + + // update the stati from the exception + if ($this->check_perms(EGW_ACL_EDIT, $egw_event['id'])) + { + $this->update_status($event, $egw_event, $recur_date); + } + elseif (isset($egw_event['participants'][$this->user])) + { + // check if current user is an attendee and tried to change his status + $this->set_status($egw_event, $this->user, + ($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $recur_date, true); + } + $Ok = $egw_event['id'] . ':' . $recur_date; + continue; // nothing more to do + } + else + { + // We need to create an new exception + $egw_event['recur_exception'] = array_unique(array_merge($egw_event['recur_exception'], array($recur_date))); + $this->update($egw_event, true); + $event['category'] = $egw_event['category']; + $cal_id = -1; + } + } + else + { + // The series event was not found + $cal_id = -1; + } + } + else + { + // We update an existing exception + $cal_id = $egw_event['id']; + $event['id'] = $egw_event['id']; + $event['category'] = $egw_event['category']; + } + } + else + { + // We handle a single event or the series master + $days = $this->so->get_recurrence_exceptions($event); + if (is_array($days)) + { + Horde::logMessage("importVCAL event\n" . print_r($event, true), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + Horde::logMessage("importVCAL days\n" . print_r($days, true), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + // remove all known "stati only" exceptions + $recur_exceptions = array(); + foreach ($event['recur_exception'] as $recur_exception) + { + if (!in_array($recur_exception, $days)) + { + $recur_exceptions[] = $recur_exception; + } + } + Horde::logMessage("importVCAL exceptions\n" . print_r($recur_exceptions, true), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $event['recur_exception'] = $recur_exceptions; } } - // check if iCal changes the organizer, which is not allowed - if ($cal_id > 0 && ($egw_event = $this->read($cal_id))) + if ($cal_id <= 0) { - $event['owner'] = $egw_event['owner']; // do NOT change the original owner - } - // check for new events if an owner is set and the current user has add rights for that owners calendar - elseif (!isset($event['owner']) || $this->check_perms(EGW_ACL_ADD,0,$event['owner'])) - { - $event['owner'] = $GLOBALS['egw_info']['user']['account_id']; // if not set the current user - } + // new entry + if ($this->isWholeDay($event) && $this->nonBlockingAllday) + { + $event['non_blocking'] = 1; + } - #error_log('ALARMS'); - //error_log(__FILE__ . __METHOD__ ."whats in event". print_r($event, true)); + if (!isset($event['owner']) + || !$this->check_perms(EGW_ACL_ADD,0,$event['owner'])) + { + // check for new events if an owner is set and the current user has add rights + // for that owners calendar; if not set the current user + $event['owner'] = $GLOBALS['egw_info']['user']['account_id']; + } + } // if an etag is given, include it in the update if (!is_null($etag)) { $event['etag'] = $etag; } - /* - We have for CREATE: - if vcal OWNER set then either this->user = OWNER or has add rights to owners Calendar - We have for UPDATE: - vcal OWNER = EGW owner - case: this->user = OWNER then all rights (C-ok,R-ok,U-?,D-ok) - */ - if($this->user == $event['owner']){ - //error_log("\nuser:$this->user owner:". print_r($event['owner'],true)); - } - /* - case: this->user = participant then only change own status - */ - if(array_key_exists($this->user,$event['participants'])){ - // error_log("\nuser:$this->user : ". print_r($egw_event,true)); - } - $original_event = $event; - //error_log(__FILE__ . __METHOD__ . "\n after case:" . print_r($event['participants'],true)); - if (!($Ok = $this->update($event, TRUE))) - { - // check if current user is an attendee and tried to change his status - //error_log(__FILE__ . __METHOD__ . "\n:in update" . print_r($egw_event,true)); - if ($Ok === false && $cal_id && ($egw_event = $this->read($cal_id)) && isset($egw_event['participants'][$this->user]) && - $egw_event['participants'][$this->user] !== $event['participants'][$this->user]) - { - //error_log(__FILE__ . __METHOD__ . "\n:set status" . print_r($egw_event,true)); - $this->set_status($egw_event,$this->user,$status = $event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'); - $Ok = $cal_id; - continue; + $original_event = $event; + + if (!($Ok = $this->update($event, true))) + { + if ($Ok === false && $cal_id > 0 && ($egw_event = $this->read($cal_id))) + { + $unchanged = true; + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + // Check if a recurring event "status only" exception is created by the client + foreach (array('uid','owner','title','description', + 'location','priority','public','special','non_blocking') as $key) + { + //Horde::logMessage('importVCAL test ' .$key . ': '. $egw_event[$key] . ' == ' .$event[$key], + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + if (!empty($event[$key]) //|| !empty($egw_event[$key])) + && $egw_event[$key] != $event[$key]) + { + $unchanged = false; + break; + } + } + } + // check if current user is an attendee and tried to change his status + if (isset($egw_event['participants'][$this->user])) + { + $this->set_status($egw_event, $this->user, + ($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $recur_date); + + $Ok = $cal_id; + continue; + } + + if ($unchanged) + { + $Ok = $cal_id; + continue; + } } break; // stop with the first error } else { - $eventID =& $Ok; - if(isset($egw_event) && $original_event['participants'] != $egw_event['participants'])$this->update_status($original_event,$egw_event); - + $eventID = &$Ok; + /* if(isset($egw_event) + && $original_event['participants'] != $egw_event['participants']) + { + $this->update_status($original_event, $egw_event, $recur_date); + } */ + + $alarms = $event['alarm']; + // handle the alarms foreach ($component->getComponents() as $valarm) { if (is_a($valarm, 'Horde_iCalendar_valarm')) { - $this->valarm2egw($alarms,$valarm); + $this->valarm2egw($alarms, $valarm); } } - if(count($alarms) > 0 || (isset($this->supportedFields['alarms']) && count($alarms) == 0)) + if (count($alarms) > 0 + || (isset($this->supportedFields['alarms']) + && count($alarms) == 0)) { // delete the old alarms $updatedEvent = $this->read($eventID); - foreach($updatedEvent['alarm'] as $alarmID => $alarmData) + foreach ($updatedEvent['alarm'] as $alarmID => $alarmData) { $this->delete_alarm($alarmID); } } - foreach($alarms as $alarm) + foreach ($alarms as $alarm) { - $alarm['offset'] = $event['start'] - $alarm['time']; + if (!isset($alarm['offset'])) + { + $alarm['offset'] = $event['start'] - $alarm['time']; + } + if (!isset($alarm['time'])) + { + $alarm['time'] = $event['start'] - $alarm['offset']; + } $alarm['owner'] = $GLOBALS['egw_info']['user']['account_id']; + $alarm['all'] = true; $this->save_alarm($eventID, $alarm); } } $cal_id = -1; } + $egw_event = $this->read($eventID); + Horde::logMessage("importVCAL:\n" . print_r($egw_event, true), + __FILE__, __LINE__, PEAR_LOG_DEBUG); } return $Ok; } @@ -1167,23 +1181,56 @@ class calendar_ical extends calendar_boupdate error_log('VALARM/TRIGGER: unsupported value type:' . $vtype); } break; - // case 'ACTION': - // break; - // case 'DISPLAY': - // break; + case 'ACTION': + case 'DISPLAY': + case 'DESCRIPTION': + case 'SUMMARY': + case 'ATTACH': + case 'ATTENDEE': + // we ignore these fields silently + break; default: - // error_log('VALARM field:' .$vattr['name'] .':' . print_r($vattrval,true) . ' HAS NO CONVERSION YET'); + error_log('VALARM field ' .$vattr['name'] . ': ' . $vattr['value'] . ' HAS NO CONVERSION YET'); } } return $count; } - function setSupportedFields($_productManufacturer='file', $_productName='') + function setSupportedFields($_productManufacturer='', $_productName='') { - // save them vor later use - $this->productManufacturer = $_productManufacturer; - $this->productName = $_productName; + $state = &$_SESSION['SyncML.state']; + if (isset($state)) { + $deviceInfo = $state->getClientDeviceInfo(); + } + + // store product manufacturer and name, to be able to use it elsewhere + if ($_productManufacturer) { + $this->productManufacturer = strtolower($_productManufacturer); + $this->productName = strtolower($_productName); + } + + if(isset($deviceInfo) && is_array($deviceInfo)) { + if(isset($deviceInfo['uidExtension']) && + $deviceInfo['uidExtension']){ + $this->uidExtension = true; + } + if(isset($deviceInfo['nonBlockingAllday']) && + $deviceInfo['nonBlockingAllday']){ + $this->nonBlockingAllday = true; + } + if(!isset($this->productManufacturer) || + $this->productManufacturer == '' || + $this->productManufacturer == 'file') { + $this->productManufacturer = strtolower($deviceInfo['manufacturer']); + } + if(!isset($this->productName) || $this->productName == '') { + $this->productName = strtolower($deviceInfo['model']); + } + } + + Horde::logMessage('setSupportedFields(' . $this->productManufacturer + . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); $defaultFields['minimal'] = array( 'public' => 'public', @@ -1196,7 +1243,7 @@ class calendar_ical extends calendar_boupdate 'recur_data' => 'recur_data', 'recur_enddate' => 'recur_enddate', 'title' => 'title', - 'alarms' => 'alarms', + 'alarm' => 'alarm', ); $defaultFields['basic'] = $defaultFields['minimal'] + array( @@ -1206,17 +1253,28 @@ class calendar_ical extends calendar_boupdate $defaultFields['nexthaus'] = $defaultFields['basic'] + array( 'participants' => 'participants', + 'uid' => 'uid', + ); + + $defaultFields['s60'] = $defaultFields['basic'] + array( + 'category' => 'category', + 'uid' => 'uid', ); $defaultFields['synthesis'] = $defaultFields['basic'] + array( - 'non_blocking' => 'non_blocking', + 'participants' => 'participants', + 'owner' => 'owner', 'category' => 'category', + 'non_blocking' => 'non_blocking', + 'uid' => 'uid', + 'reference' => 'reference', ); $defaultFields['evolution'] = $defaultFields['basic'] + array( 'participants' => 'participants', 'owner' => 'owner', 'category' => 'category', + 'uid' => 'uid', ); $defaultFields['full'] = $defaultFields['basic'] + array( @@ -1224,14 +1282,16 @@ class calendar_ical extends calendar_boupdate 'owner' => 'owner', 'category' => 'category', 'non_blocking' => 'non_blocking', + 'uid' => 'uid', + 'reference' => 'reference', ); - switch(strtolower($_productManufacturer)) + switch($this->productManufacturer) { case 'nexthaus corporation': case 'nexthaus corp': - switch(strtolower($_productName)) + switch($this->productName) { default: $this->supportedFields = $defaultFields['nexthaus']; @@ -1242,7 +1302,7 @@ class calendar_ical extends calendar_boupdate // multisync does not provide anymore information then the manufacturer // we suppose multisync with evolution case 'the multisync project': - switch(strtolower($_productName)) + switch($this->productName) { default: $this->supportedFields = $defaultFields['basic']; @@ -1250,8 +1310,21 @@ class calendar_ical extends calendar_boupdate } break; + case 'siemens': + switch($this->productName) + { + case 'sx1': + $this->supportedFields = $defaultFields['minimal']; + break; + default: + error_log("Unknown Siemens phone '$_productName', using minimal set"); + $this->supportedFields = $defaultFields['minimal']; + break; + } + break; + case 'nokia': - switch(strtolower($_productName)) + switch($this->productName) { case 'e61': $this->supportedFields = $defaultFields['minimal']; @@ -1259,7 +1332,10 @@ class calendar_ical extends calendar_boupdate case 'e51': case 'e90': case 'e71': - $this->supportedFields = $defaultFields['basic']; + case 'e66': + case '6120c': + case 'nokia 6131': + $this->supportedFields = $defaultFields['s60']; break; default: error_log("Unknown Nokia phone '$_productName', assuming E61"); @@ -1270,26 +1346,27 @@ class calendar_ical extends calendar_boupdate case 'sonyericsson': case 'sony ericsson': - switch(strtolower($_productName)) + switch($this->productName) { case 'd750i': case 'p910i': + case 'g705i': $this->supportedFields = $defaultFields['basic']; break; default: - error_log("Unknown Sony Ericsson phone '$_productName' assuming d750i"); + error_log("Unknown Sony Ericsson phone '$this->productName' assuming d750i"); $this->supportedFields = $defaultFields['basic']; break; } break; case 'synthesis ag': - switch(strtolower($_productName)) + switch($this->productName) { case 'sysync client pocketpc std': case 'sysync client pocketpc pro': - $this->supportedFields = $defaultFields['full']; - break; + case 'sysync client iphone contacts': + case 'sysync client iphone contacts+todoz': default: $this->supportedFields = $defaultFields['synthesis']; break; @@ -1320,335 +1397,589 @@ class calendar_ical extends calendar_boupdate // the fallback for SyncML default: - error_log("Unknown calendar SyncML client: manufacturer='$_productManufacturer' product='$_productName'"); - $this->supportedFields = $defaultFields['full']; + error_log("Unknown calendar SyncML client: manufacturer='$this->productManufacturer' product='$this->productName'"); + $this->supportedFields = $defaultFields['synthesis']; break; } } - function icaltoegw($_vcalData) + function icaltoegw($_vcalData, $cal_id=-1) { - // our (patched) horde classes, do NOT unfold folded lines, which causes a lot trouble in the import - $_vcalData = preg_replace("/[\r\n]+ /",'',$_vcalData); + $event = false; // returning false, if file contains no components $vcal = new Horde_iCalendar; - if(!$vcal->parsevCalendar($_vcalData)) - { - return FALSE; - } + if (!$vcal->parsevCalendar($_vcalData)) return false; - if(!is_array($this->supportedFields)) - { - $this->setSupportedFields(); - } - //echo "supportedFields="; _debug_array($this->supportedFields); + $version = $vcal->getAttribute('VERSION'); - $Ok = false; // returning false, if file contains no components - foreach($vcal->getComponents() as $component) + if (!is_array($this->supportedFields)) $this->setSupportedFields(); + + foreach ($vcal->getComponents() as $component) { - if(is_a($component, 'Horde_iCalendar_vevent')) + if (is_a($component, 'Horde_iCalendar_vevent')) { - $supportedFields = $this->supportedFields; - #$event = array('participants' => array()); - $event = array(); - $alarms = array(); - $vcardData = array('recur_type' => 0); - - // lets see what we can get from the vcard - foreach($component->_attributes as $attributes) + // We expect only a single VEVENT + $event = $this->vevent2egw($component, $version, $this->supportedFields); + if ($event) { - switch($attributes['name']) + if ($cal_id > 0) { + $event['id'] = $cal_id; + } + else { - case 'AALARM': - case 'DALARM': - if (preg_match('/.*Z$/',$attributes['value'],$matches)) - { - $alarmTime = $vcal->_parseDateTime($attributes['value']); - $alarms[$alarmTime] = array( - 'time' => $alarmTime - ); - } - break; - case 'CLASS': - $vcardData['public'] = (int)(strtolower($attributes['value']) == 'public'); - break; - case 'DESCRIPTION': - $vcardData['description'] = $attributes['value']; - break; - case 'DTEND': - /* write start + end of whole day events as dates - DTSTART;VALUE=DATE:20090317 implies 000000Z - DTEND;VALUE=DATE:20090317 implies 235959Z - And DEFINES an ALLDAY event - and as such is "inclusive" - some clients(Lightning, Korganiser?) need an "exclusive" - Legacy: EGW held 235900 As DTEND allday, such entries in databases will exist - EGW is internally correct on 235959 "inclusive" - */ - //error_log(__FILE__ . __METHOD__. "DTEND:". print_r($attributes['value'],true)); - - if(date('H:i:s',$attributes['value']) == '00:00:00') - $attributes['value']--; - $vcardData['end'] = $attributes['value']; - break; - case 'DTSTART': - $vcardData['start'] = $attributes['value']; - break; - case 'LOCATION': - $vcardData['location'] = $attributes['value']; - break; - case 'RRULE': - $recurence = $attributes['value']; - $type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0]; - // vCard 2.0 values for all types - if (preg_match('/UNTIL=([0-9T]+)/',$recurence,$matches)) - { - $vcardData['recur_enddate'] = $vcal->_parseDateTime($matches[1]); - } - if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches)) - { - $vcardData['recur_interval'] = (int) $matches[1]; - } - $vcardData['recur_data'] = 0; - switch($type) - { - case 'W': - case 'WEEKLY': - $days = array(); - if(preg_match('/W(\d+) (.*) (.*)/',$recurence, $recurenceMatches)) // 1.0 - { - $vcardData['recur_interval'] = $recurenceMatches[1]; - $days = explode(' ',trim($recurenceMatches[2])); - if($recurenceMatches[3] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[3]); - $recur_days = $this->recur_days_1_0; - } - elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0 - { - $days = explode(',',$recurenceMatches[1]); - $recur_days = $this->recur_days; - } - if ($days) - { - foreach($recur_days as $id => $day) - { - if (in_array(strtoupper(substr($day,0,2)),$days)) - { - $vcardData['recur_data'] |= $id; - } - } - $vcardData['recur_type'] = MCAL_RECUR_WEEKLY; - } - break; - - case 'D': // 1.0 - if(!preg_match('/D(\d+) (.*)/',$recurence, $recurenceMatches)) break; - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[2]); - // fall-through - case 'DAILY': // 2.0 - $vcardData['recur_type'] = MCAL_RECUR_DAILY; - break; - - case 'M': - if(preg_match('/MD(\d+) (.*)/',$recurence, $recurenceMatches)) - { - $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY; - if($recurenceMatches[1] > 1) - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[2]); - } - elseif(preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches)) - { - $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_WDAY; - if($recurenceMatches[1] > 1) - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[4] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[4]); - } - break; - case 'MONTHLY': - $vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ? - MCAL_RECUR_MONTHLY_WDAY : MCAL_RECUR_MONTHLY_MDAY; - break; - - case 'Y': // 1.0 - if(!preg_match('/YM(\d+) (.*)/',$recurence, $recurenceMatches)) break; - $vcardData['recur_interval'] = $recurenceMatches[1]; - if($recurenceMatches[2] != '#0') - $vcardData['recur_enddate'] = $vcal->_parseDateTime($recurenceMatches[2]); - // fall-through - case 'YEARLY': // 2.0 - $vcardData['recur_type'] = MCAL_RECUR_YEARLY; - break; - } - break; - case 'EXDATE': - $vcardData['recur_exception'] = $attributes['value']; - break; - case 'SUMMARY': - $vcardData['title'] = $attributes['value']; - break; - case 'UID': - $event['uid'] = $vcardData['uid'] = $attributes['value']; - if ($cal_id <= 0 && !empty($vcardData['uid']) && ($uid_event = $this->read($vcardData['uid']))) - { - $event['id'] = $uid_event['id']; - unset($uid_event); - } - // not use weak uids that might come from syncml clients - if (isset($event['uid']) && (strlen($event['uid']) < 20 || is_numeric($event['uid']))) - { - error_log ("unset weak uid"); - unset ($event['uid']); - } - break; - case 'TRANSP': - $vcardData['non_blocking'] = $attributes['value'] == 'TRANSPARENT'; - break; - case 'PRIORITY': - if ($this->productManufacturer == 'nexthaus corporation' - || $this->productManufacturer == 'nexthaus corp') - { - $vcardData['priority'] = $attributes['value'] == 1 ? 3 : 2; // 1=high, 2=normal - } - else - { - $vcardData['priority'] = (int) $this->priority_ical2egw[$attributes['value']]; - } - break; - case 'CATEGORIES': - if ($attributes['value']) - { - $vcardData['category'] = $this->find_or_add_categories(explode(',',$attributes['value'])); - } - else - { - $vcardData['category'] = array(); - } - break; - case 'ATTENDEE': - if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) && - ($uid = $GLOBALS['egw']->accounts->name2id($matches[1],'account_email'))) - { - $event['participants'][$uid] = isset($attributes['params']['PARTSTAT']) ? - $this->status_ical2egw[strtoupper($attributes['params']['PARTSTAT'])] : - ($uid == $event['owner'] ? 'A' : 'U'); - } - break; - case 'ORGANIZER': // will be written direct to the event - if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) && - ($uid = $GLOBALS['egw']->accounts->name2id($matches[1],'account_email'))) - { - $event['owner'] = $uid; - } - break; - case 'CREATED': // will be written direct to the event - if ($event['modified']) break; - // fall through - case 'LAST-MODIFIED': // will be written direct to the event - $event['modified'] = $attributes['value']; - break; + if($this->isWholeDay($event) + && $this->nonBlockingAllday) + { + $event['non_blocking'] = 1; + } } - } - - // check if the entry is a birthday - // this field is only set from NOKIA clients - $agendaEntryType = $component->getAttribute('X-EPOCAGENDAENTRYTYPE'); - if (!is_a($agendaEntryType, 'PEAR_Error')) { - if(strtolower($agendaEntryType) == 'anniversary') { - $event['special'] = '1'; - $vcardData['end'] = $vcardData['start'] + 86399; - } - } - - if(!empty($vcardData['recur_enddate'])) - { - // reset recure_enddate to 00:00:00 on the last day - $vcardData['recur_enddate'] = mktime(0, 0, 0, - date('m',$vcardData['recur_enddate']), - date('d',$vcardData['recur_enddate']), - date('Y',$vcardData['recur_enddate']) - ); - } - //echo "event=";_debug_array($vcardData); - - while(($fieldName = array_shift($supportedFields))) - { - switch($fieldName) + if ($this->productManufacturer == '' && $this->productName == '' + && !empty($event['recur_enddate'])) { - case 'recur_interval': - case 'recur_enddate': - case 'recur_data': - case 'recur_exception': - case 'alarms': - // not handled here - break; - case 'recur_type': - $event['recur_type'] = $vcardData['recur_type']; - if ($event['recur_type'] != MCAL_RECUR_NONE) - { - foreach(array('recur_interval','recur_enddate','recur_data','recur_exception') as $r) - { - if(isset($vcardData[$r])) - { - $event[$r] = $vcardData[$r]; - } - } - } - break; - default: - if (isset($vcardData[$fieldName])) - { - $event[$fieldName] = $vcardData[$fieldName]; - } - break; + // syncevolution needs an adjusted recur_enddate + $event['recur_enddate'] = (int)$event['recur_enddate'] + 86400; } } - return $event; } } - return false; } - function search($_vcalData, $contentID=null) + /** + * Parse an VEVENT + * + * @param array $component VEVENT + * @param string $version vCal version (1.0/2.0) + * @param array $supportedFields supported fields of the device + * + * @return array|boolean event on success, false on failure + */ + function vevent2egw(&$component, $version, $supportedFields) { - if(!$event = $this->icaltoegw($_vcalData)) { - return false; + if (!is_a($component, 'Horde_iCalendar_vevent')) return false; + + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; } - if ($event['uid'] && ($uidmatch = $this->read($event['uid']))) + else { - return $uidmatch['id']; + $minimum_uid_length = 8; } - $query = array( - 'cal_start='.$this->date2ts($event['start'],true), // true = Server-time - 'cal_end='.$this->date2ts($event['end'],true), + $isDate = false; + $event = array(); + $alarms = array(); + $vcardData = array( + 'recur_type' => MCAL_RECUR_NONE, + 'recur_exception' => array(), ); - - if ($contentID) { - $query[] = 'egw_cal.cal_id='.(int)$contentID; - } - - #foreach(array('title','location','priority','public','non_blocking') as $name) { - foreach(array('title','location','public','non_blocking') as $name) { - if (isset($event[$name])) $query['cal_'.$name] = $event[$name]; - } - - if($foundEvents = parent::search(array( - 'user' => $this->user, - 'query' => $query, - ))) { - if(is_array($foundEvents)) { - $event = array_shift($foundEvents); - return $event['id']; + // we parse DTSTART first + foreach ($component->_attributes as $attributes) + { + if ($attributes['name'] == 'DTSTART') + { + if (isset($attributes['params']['VALUE']) + && $attributes['params']['VALUE'] == 'DATE') + { + $isDate = true; + } + $vcardData['start'] = $attributes['value']; } } - return false; + if (!isset($vcardData['start'])) return false; // not a valid entry + + // lets see what we can get from the vcard + foreach ($component->_attributes as $attributes) + { + switch ($attributes['name']) + { + case 'AALARM': + case 'DALARM': + $alarmTime = $attributes['value']; + $alarms[$alarmTime] = array( + 'time' => $alarmTime + ); + break; + case 'CLASS': + $vcardData['public'] = (int)(strtolower($attributes['value']) == 'public'); + break; + case 'DESCRIPTION': + $vcardData['description'] = $attributes['value']; + if (preg_match('/\s*\[UID:(.+)?\]/Usm', $attributes['value'], $matches)) { + if (!isset($vCardData['uid']) + && strlen($matches[1]) >= $minimum_uid_length) { + $vcardData['uid'] = $matches[1]; + } + } + break; + case 'DTEND': + $dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']); + if(date('H:i:s',$dtend_ts) == '00:00:00') { + $dtend_ts -= 60; + } + $vcardData['end'] = $dtend_ts; + break; + case 'RECURRENCE-ID': + $vcardData['reference'] = $attributes['value']; + break; + case 'LOCATION': + $vcardData['location'] = $attributes['value']; + break; + case 'RRULE': + $recurence = $attributes['value']; + $type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0]; + // vCard 2.0 values for all types + if (preg_match('/UNTIL=([0-9T]+)/',$recurence,$matches)) + { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($matches[1]); + } + elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches)) + { + $vcardData['recur_count'] = (int)$matches[1]; + } + if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches)) + { + // 1 is invalid,, egw uses 0 for interval + $vcardData['recur_interval'] = (int) $matches[1] != 0 ? (int) $matches[1] : 0; + } + if (!isset($vcardData['start'])) // it might not yet be set, because the RRULE is before it + { + $vcardData['start'] = self::_get_attribute($component->_attributes,'DTSTART'); + $vcardData['end'] = self::_get_attribute($component->_attributes,'DTEND'); + } + $vcardData['recur_data'] = 0; + switch($type) + { + case 'W': + case 'WEEKLY': + $days = array(); + if(preg_match('/W(\d+) (.*) (.*)/',$recurence, $recurenceMatches)) // 1.0 + { + $vcardData['recur_interval'] = $recurenceMatches[1]; + $days = explode(' ',trim($recurenceMatches[2])); + if($recurenceMatches[3] != '#0') + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[3]); + $recur_days = $this->recur_days_1_0; + } + elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches)) // 2.0 + { + $days = explode(',',$recurenceMatches[1]); + $recur_days = $this->recur_days; + } + else // no day given, use the day of dtstart + { + $vcardData['recur_data'] |= 1 << (int)date('w',$vcardData['start']); + $vcardData['recur_type'] = MCAL_RECUR_WEEKLY; + } + if ($days) + { + foreach($recur_days as $id => $day) { + if (in_array(strtoupper(substr($day,0,2)),$days)) { + $vcardData['recur_data'] |= $id; + } + } + $vcardData['recur_type'] = MCAL_RECUR_WEEKLY; + } + + if (!empty($vcardData['recur_count'])) + { + $vcardData['recur_enddate'] = mktime(0,0,0, + date('m',$vcardData['start']), + date('d',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1)*7), + date('Y',$vcardData['start'])); + } + break; + + case 'D': // 1.0 + if(preg_match('/D(\d+) #(.\d)/', $recurence, $recurenceMatches)) { + $vcardData['recur_interval'] = $recurenceMatches[1]; + if($recurenceMatches[2] > 0 && $vcardData['end']) { + $vcardData['recur_enddate'] = mktime( + date('H', $vcardData['end']), + date('i', $vcardData['end']), + date('s', $vcardData['end']), + date('m', $vcardData['end']), + date('d', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']), + date('Y', $vcardData['end']) + ); + } + } elseif(preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches)) { + $vcardData['recur_interval'] = $recurenceMatches[1]; + if($recurenceMatches[2] != '#0') { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + } + } else { + break; + } + // fall-through + case 'DAILY': // 2.0 + $vcardData['recur_type'] = MCAL_RECUR_DAILY; + + if (!empty($vcardData['recur_count'])) + { + $vcardData['recur_enddate'] = mktime(0,0,0, + date('m',$vcardData['start']), + date('d',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1)), + date('Y',$vcardData['start'])); + } + break; + + case 'M': + if (preg_match('/MD(\d+) #(.\d)/', $recurence, $recurenceMatches)) { + $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY; + $vcardData['recur_interval'] = $recurenceMatches[1]; + if($recurenceMatches[2] > 0 && $vcardData['end']) { + $vcardData['recur_enddate'] = mktime( + date('H', $vcardData['end']), + date('i', $vcardData['end']), + date('s', $vcardData['end']), + date('m', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']), + date('d', $vcardData['end']), + date('Y', $vcardData['end']) + ); + } + } + elseif (preg_match('/MD(\d+) (.*)/',$recurence, $recurenceMatches)) + { + $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY; + if($recurenceMatches[1] > 1) + { + $vcardData['recur_interval'] = $recurenceMatches[1]; + } + if($recurenceMatches[2] != '#0') + { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + } + } + elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches)) + { + $vcardData['recur_type'] = MCAL_RECUR_MONTHLY_WDAY; + if ($recurenceMatches[1] > 1) + { + $vcardData['recur_interval'] = $recurenceMatches[1]; + } + if ($recurenceMatches[4] != '#0') + { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]); + } + } + break; + case 'MONTHLY': + $vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ? + MCAL_RECUR_MONTHLY_WDAY : MCAL_RECUR_MONTHLY_MDAY; + + if (!empty($vcardData['recur_count'])) + { + $vcardData['recur_enddate'] = mktime(0,0,0, + date('m',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1)), + date('d',$vcardData['start']), + date('Y',$vcardData['start'])); + } + break; + + case 'Y': // 1.0 + if (preg_match('/YM(\d+) #(.\d)/', $recurence, $recurenceMatches)) + { + $vcardData['recur_interval'] = $recurenceMatches[1]; + if ($recurenceMatches[2] > 0 && $vcardData['end']) + { + $vcardData['recur_enddate'] = mktime( + date('H', $vcardData['end']), + date('i', $vcardData['end']), + date('s', $vcardData['end']), + date('m', $vcardData['end']), + date('d', $vcardData['end']), + date('Y', $vcardData['end']) + ($recurenceMatches[2] * $vcardData['recur_interval']) + ); + } + } + elseif (preg_match('/YM(\d+) (.*)/',$recurence, $recurenceMatches)) + { + $vcardData['recur_interval'] = $recurenceMatches[1]; + if($recurenceMatches[2] != '#0') { + $vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]); + } + } else { + break; + } + // fall-through + case 'YEARLY': // 2.0 + $vcardData['recur_type'] = MCAL_RECUR_YEARLY; + + if (!empty($vcardData['recur_count'])) + { + $vcardData['recur_enddate'] = mktime(0,0,0, + date('m',$vcardData['start']), + date('d',$vcardData['start']), + date('Y',$vcardData['start']) + ($vcardData['recur_interval']*($vcardData['recur_count']-1))); + } + break; + } + break; + case 'EXDATE': + if ((isset($attributes['params']['VALUE']) + && $attributes['params']['VALUE'] == 'DATE') || + (!isset($attributes['params']['VALUE']) && $isDate)) + { + $days = array(); + $hour = date('H', $vcardData['start']); + $minutes = date('i', $vcardData['start']); + $seconds = date('s', $vcardData['start']); + foreach ($attributes['values'] as $day) + { + $days[] = mktime( + $hour, + $minutes, + $seconds, + $day['month'], + $day['mday'], + $day['year']); + } + $vcardData['recur_exception'] = array_merge($vcardData['recur_exception'], $days); + } + else + { + $vcardData['recur_exception'] = array_merge($vcardData['recur_exception'], $attributes['values']); + } + break; + case 'SUMMARY': + $vcardData['title'] = $attributes['value']; + break; + case 'UID': + if (strlen($attributes['value']) >= $minimum_uid_length) + { + $event['uid'] = $vcardData['uid'] = $attributes['value']; + } + break; + case 'TRANSP': + if ($version == '1.0') + { + $vcardData['non_blocking'] = ($attributes['value'] == 1); + } + else + { + $vcardData['non_blocking'] = ($attributes['value'] == 'TRANSPARENT'); + } + break; + case 'PRIORITY': + $vcardData['priority'] = (int) $this->priority_ical2egw[$attributes['value']]; + break; + case 'CATEGORIES': + if ($attributes['value']) + { + if($version == '1.0') + { + $vcardData['category'] = $this->find_or_add_categories(explode(';',$attributes['value'])); + } + else + { + $vcardData['category'] = $this->find_or_add_categories(explode(',',$attributes['value'])); + } + } + else + { + $vcardData['category'] = array(); + } + break; + case 'ATTENDEE': + case 'ORGANIZER': // will be written direct to the event + $cn = ''; + if (preg_match('/MAILTO:([@.a-z0-9_-]+)|MAILTO:"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i', + $attributes['value'],$matches)) { + $email = $matches[1] ? $matches[1] : $matches[3]; + $cn = isset($matches[2]) ? $matches[2]: ''; + } + elseif (preg_match('/"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i', + $attributes['value'],$matches)) + { + $cn = $matches[1]; + $email = $matches[2]; + } + elseif (strpos($attributes['value'],'@') !== false) + { + $email = $attributes['value']; + } + else + { + $email = false; // no email given + } + $searcharray = array(); + if ($email) + { + $searcharray = array('email' => $email, 'email_home' => $email); + } + if (isset($attributes['params']['CN']) && $attributes['params']['CN']) + { + if ($attributes['params']['CN'][0] == '"' + && substr($attributes['params']['CN'],-1) == '"') + { + $attributes['params']['CN'] = substr($attributes['params']['CN'],1,-1); + } + $searcharray['n_fn'] = $attributes['params']['CN']; + } + elseif ($cn) + { + $searcharray['n_fn'] = $cn; + } + if (($uid = $attributes['params']['X-EGROUPWARE-UID']) + && ($info = $this->resource_info($uid)) + && (!$email || $info['email'] == $email)) + { + // we use the (checked) X-EGROUPWARE-UID + } + /*elseif($attributes['params']['CUTYPE'] == 'RESOURCE') + { + + }*/ + elseif ($attributes['value'] == 'Unknown') + { + $uid = $GLOBALS['egw_info']['user']['account_id']; + } + elseif ($email && ($uid = $GLOBALS['egw']->accounts->name2id($email,'account_email'))) + { + // we use the account we found + } + elseif(!$searcharray) + { + continue; // participants without email AND CN --> ignore it + } + elseif ((list($data) = ExecMethod2('addressbook.addressbook_bo.search',$searcharray, + array('id','egw_addressbook.account_id as account_id','n_fn'),'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC','','',false,'OR'))) + { + $uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id']; + } + else + { + if (!$email) + { + $email = 'no-email@egroupware.org'; // set dummy email to store the CN + } + $uid = 'e'.($attributes['params']['CN'] ? $attributes['params']['CN'].' <'.$email.'>' : $email); + } + switch($attributes['name']) + { + case 'ATTENDEE': + if (isset($attributes['params']['PARTSTAT'])) + { + $event['participants'][$uid] = $this->status_ical2egw[strtoupper($attributes['params']['PARTSTAT'])]; + } + elseif (isset($attributes['params']['STATUS'])) + { + $event['participants'][$uid] = $this->status_ical2egw[strtoupper($attributes['params']['STATUS'])]; + } + else + { + $event['participants'][$uid] = ($uid == $event['owner'] ? 'A' : 'U'); + } + break; + case 'ORGANIZER': + if (is_numeric($uid)) + { + $event['owner'] = $uid; + } + else + { + $event['owner'] = $this->user; + } + $event['participants'][$uid] = 'A'; + break; + } + break; + case 'CREATED': // will be written direct to the event + if ($event['modified']) break; + // fall through + case 'LAST-MODIFIED': // will be written direct to the event + $event['modified'] = $attributes['value']; + break; + } + } + + // check if the entry is a birthday + // this field is only set from NOKIA clients + $agendaEntryType = $component->getAttribute('X-EPOCAGENDAENTRYTYPE'); + if (!is_a($agendaEntryType, 'PEAR_Error')) + { + if (strtolower($agendaEntryType) == 'anniversary') + { + $event['special'] = '1'; + $event['non_blocking'] = 1; + // make it a whole day event for eGW + $vcardData['end'] = $vcardData['start'] + 86399; + } + elseif(strtolower($agendaEntryType) == 'event') + { + $event['special'] = '2'; + $event['non_blocking'] = 1; + } + } + + if (!empty($vcardData['recur_enddate'])) + { + // reset recure_enddate to 00:00:00 on the last day + $vcardData['recur_enddate'] = mktime(0, 0, 0, + date('m',$vcardData['recur_enddate']), + date('d',$vcardData['recur_enddate']), + date('Y',$vcardData['recur_enddate']) + ); + } + + $event['priority'] = 2; // default + $event['alarm'] = $alarms; + + // now that we know what the vard provides, + // we merge that data with the information we have about the device + while (($fieldName = array_shift($supportedFields))) + { + switch($fieldName) + { + case 'recur_interval': + case 'recur_enddate': + case 'recur_data': + case 'recur_exception': + // not handled here + break; + + case 'recur_type': + $event['recur_type'] = $vcardData['recur_type']; + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + $event['reference'] = 0; + foreach(array('recur_interval','recur_enddate','recur_data','recur_exception') as $r) + { + if(isset($vcardData[$r])) + { + $event[$r] = $vcardData[$r]; + } + } + } + break; + + default: + if (isset($vcardData[$fieldName])) + { + $event[$fieldName] = $vcardData[$fieldName]; + } + break; + } + } + //Horde::logMessage("vevent2egw:\n" . print_r($event, true), + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $event; + } + + function search($_vcalData, $contentID=null, $relax=false) + { + $result = false; + + if($event = $this->icaltoegw($_vcalData)) + { + if ($contentID) { + $event['id'] = $contentID; + } + $result = $this->find_event($event, $relax); + } + return $result; } /** @@ -1656,9 +1987,10 @@ class calendar_ical extends calendar_boupdate * * @param int $user account_id * @param mixed $end=null end-date, default now+1 month + * @param boolean $servertime=false if true, use severtime for dates * @return string */ - function freebusy($user,$end=null) + function freebusy($user,$end=null,$servertime=false) { if (!$end) $end = $this->now_su + 100*DAY_s; // default next 100 days @@ -1674,15 +2006,31 @@ class calendar_ical extends calendar_boupdate $GLOBALS['egw']->accounts->id2name($user,'account_lastname'), $GLOBALS['egw']->translation->charset(),'utf-8'), ); - foreach(array( - 'URL' => $this->freebusy_url($user), - 'DTSTART' => $this->date2ts($this->now_su,true), // true = server-time - 'DTEND' => $this->date2ts($end,true), // true = server-time - 'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'), - 'DTSTAMP' => time(), - ) as $attr => $value) + if ($servertime) { - $vfreebusy->setAttribute($attr, $value, $parameters[$name]); + foreach(array( + 'URL' => $this->freebusy_url($user), + 'DTSTART' => date('Ymd\THis',$this->date2ts($this->now_su,true)), // true = server-time + 'DTEND' => date('Ymd\THis',$this->date2ts($end,true)), // true = server-time + 'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'), + 'DTSTAMP' => date('Ymd\THis',time()), + ) as $attr => $value) + { + $vfreebusy->setAttribute($attr, $value); + } + } + else + { + foreach(array( + 'URL' => $this->freebusy_url($user), + 'DTSTART' => $this->date2ts($this->now_su,true), // true = server-time + 'DTEND' => $this->date2ts($end,true), // true = server-time + 'ORGANIZER' => $GLOBALS['egw']->accounts->id2name($user,'account_email'), + 'DTSTAMP' => time(), + ) as $attr => $value) + { + $vfreebusy->setAttribute($attr, $value); + } } $fbdata = parent::search(array( 'start' => $this->now_su, @@ -1697,10 +2045,20 @@ class calendar_ical extends calendar_boupdate { if ($event['non_blocking']) continue; - $vfreebusy->setAttribute('FREEBUSY',array(array( - 'start' => $event['start'], - 'end' => $event['end'], - ))); + if ($servertime) + { + $vfreebusy->setAttribute('FREEBUSY',array(array( + 'start' => date('Ymd\THis',$event['start']), + 'end' => date('Ymd\THis',$event['end']), + ))); + } + else + { + $vfreebusy->setAttribute('FREEBUSY',array(array( + 'start' => $event['start'], + 'end' => $event['end'], + ))); + } } } $vcal->addComponent($vfreebusy); @@ -1708,36 +2066,35 @@ class calendar_ical extends calendar_boupdate return $vcal->exportvCalendar(); } - function update_status($new_event,$old_event) + /** + * update the status of all participant for a given recurrence or for all recurrences since now (includes recur_date=0) + * + * @param array $new_event event-array with the new stati + * @param array $old_event event-array with the old stati + * @param int $recur_date=0 date to change, or 0 = all since now + */ + function update_status($new_event, $old_event , $recur_date) { - $modified = $added = $deleted = array(); - //error_log(__FILE__ . __METHOD__ . "\nnew_event:" . print_r($new_event,true)); - - // check the old list against the new list and write the changes - foreach($old_event['participants'] as $old_userid => $old_status) + //error_log(__FILE__ . __METHOD__ . "\nold_event:" . print_r($old_event, true) + // . "\nnew_event:" . print_r($new_event, true)); + + // check the old list against the new list + foreach ($old_event['participants'] as $userid => $status) { - if(isset($new_event['participants'][$old_userid])){ - if($new_event['participants'][$old_userid] != $old_event['participants'][$old_userid]){ - //$modified[$old_userid] = $new_event['participants'][$old_userid]; - $this->set_status($old_event,$old_userid,$status = $new_event['participants'][$old_userid] ? $new_event['participants'][$old_userid] : 'R'); - } - } - else - { - // this doesn't get written from here at the moment - $deleted[$old_userid] = $old_status; + if(!isset($new_event['participants'][$userid])){ + // Attendee will be deleted this way + $new_event['participants'][$userid] = 'G'; + } elseif ($new_event['participants'][$userid] == $status){ + // Same status -- nothing to do. + unset($new_event['participants'][$userid]); } } - // Find new participants ... - foreach($new_event['participants'] as $new_userid => $new_status) + // write the changes + foreach ($new_event['participants'] as $userid => $status) { - if(!isset($old_event['participants'][$new_userid])) - { - // this doesn't get written from here at the moment - $added[$new_userid] = 'U'; - } + //error_log(__FILE__ . __METHOD__ . "\n$userid => $status:"); + $this->set_status($old_event, $userid, $status, $recur_date, true); } } - } diff --git a/calendar/inc/class.calendar_sif.inc.php b/calendar/inc/class.calendar_sif.inc.php index 85c1338a8e..13ae2c6fc0 100644 --- a/calendar/inc/class.calendar_sif.inc.php +++ b/calendar/inc/class.calendar_sif.inc.php @@ -4,6 +4,7 @@ * * @link http://www.egroupware.org * @author Lars Kneschke + * @author Joerg Lehrke * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package calendar * @subpackage export @@ -21,6 +22,7 @@ class calendar_sif extends calendar_boupdate 'Start' => 'start', 'End' => 'end', 'AllDayEvent' => 'alldayevent', + 'Attendees' => '', 'BillingInformation' => '', 'Body' => 'description', 'BusyStatus' => '', @@ -33,6 +35,11 @@ class calendar_sif extends calendar_boupdate 'Mileage' => '', 'ReminderMinutesBeforeStart' => 'reminderstart', 'ReminderSet' => 'reminderset', + 'ReminderSoundFile' => '', + 'ReminderOptions' => '', + 'ReminderInterval' => '', + 'ReminderRepeatCount' => '', + 'Exceptions' => '', 'ReplyTime' => '', 'Sensitivity' => 'public', 'Subject' => 'title', @@ -51,6 +58,11 @@ class calendar_sif extends calendar_boupdate // the calendar event array var $event; + // device specific settings + var $productName = 'mozilla plugin'; + var $productSoftwareVersion = '0.3'; + var $uidExtension = false; + // constants for recurence type const olRecursDaily = 0; const olRecursWeekly = 1; @@ -68,6 +80,10 @@ class calendar_sif extends calendar_boupdate const olFriday = 32; const olSaturday = 64; + // standard headers + const xml_decl = ''; + const SIF_decl = '1.1'; + function startElement($_parser, $_tag, $_attributes) { } @@ -87,14 +103,13 @@ class calendar_sif extends calendar_boupdate $vcal = new Horde_iCalendar; $finalEvent = array(); $sysCharSet = $GLOBALS['egw']->translation->charset(); - $sifData = base64_decode($_sifdata); #error_log($sifData); - $tmpfname = tempnam('/tmp/sync/contents','sife_'); + #$tmpfname = tempnam('/tmp/sync/contents','sife_'); - $handle = fopen($tmpfname, "w"); - fwrite($handle, $sifData); - fclose($handle); + #$handle = fopen($tmpfname, "w"); + #fwrite($handle, $sifData); + #fclose($handle); $this->xml_parser = xml_parser_create('UTF-8'); xml_set_object($this->xml_parser, $this); @@ -111,11 +126,13 @@ class calendar_sif extends calendar_boupdate #error_log(print_r($this->event, true)); foreach($this->event as $key => $value) { + $value = preg_replace('/<\!\[CDATA\[(.+)\]\]>/Usim', '$1', $value); $value = $GLOBALS['egw']->translation->convert($value, 'utf-8', $sysCharSet); #error_log("$key => $value"); switch($key) { case 'alldayevent': if($value == 1) { + $finalEvent['whole_day'] = true; $startParts = explode('-',$this->event['start']); $finalEvent['start'] = mktime(0, 0, 0, $startParts[1], $startParts[2], $startParts[0]); $endParts = explode('-',$this->event['end']); @@ -191,6 +208,11 @@ class calendar_sif extends calendar_boupdate // do nothing, get's handled in isrecuring clause break; + case 'description': + if (preg_match('/\s*\[UID:(.+)?\]/Usm', $value, $matches)) { + $finalEvent['uid'] = $matches[1]; + } + default: $finalEvent[$key] = $value; break; @@ -205,69 +227,32 @@ class calendar_sif extends calendar_boupdate return $finalEvent; } - function search($_sifdata, $contentID=null) { - if(!$event = $this->siftoegw($_sifdata)) { - return false; - } + function search($_sifdata, $contentID=null, $relax=false) + { + $result = false; - $query = array( - 'cal_start='.$this->date2ts($event['start'],true), // true = Server-time - 'cal_end='.$this->date2ts($event['end'],true), - ); - - if ($contentID) { - $query[] = 'egw_cal.cal_id='.(int)$contentID; - } - - #foreach(array('title','location','priority','public','non_blocking') as $name) { - foreach(array('title','location','public','non_blocking') as $name) { - if (isset($event[$name])) $query['cal_'.$name] = $event[$name]; - } - - if($foundEvents = parent::search(array( - 'user' => $this->user, - 'query' => $query, - ))) { - if(is_array($foundEvents)) { - $event = array_shift($foundEvents); - return $event['id']; + if($event = $this->siftoegw($_sifdata)) + { + if ($contentID) { + $event['id'] = $contentID; } + $result = $this->find_event($event, $relax); } - return false; - - $search['start'] = $event['start']; - $search['end'] = $event['end']; - - unset($event['description']); - unset($event['start']); - unset($event['end']); - - foreach($event as $key => $value) { - if (substr($key,0,6) != 'recur_' && substr($key,0,5) != 'alarm') { - $search['query']['cal_'.$key] = $value; - } else { - #$search['query'][$key] = $value; - } - } - - if($foundEvents = parent::search($search)) { - if(is_array($foundEvents)) { - $event = array_shift($foundEvents); - return $event['id']; - } - } - - return false; + return $result; } /** * @return int contact id * @param string $_vcard the vcard * @param int $_abID the internal addressbook id + * @param boolean $merge=false merge data with existing entry * @desc import a vard into addressbook */ - function addSIF($_sifdata, $_calID) + function addSIF($_sifdata, $_calID, $merge=false) { + $state = &$_SESSION['SyncML.state']; + $deviceInfo = $state->getClientDeviceInfo(); + $calID = false; #error_log('ABID: '.$_abID); @@ -282,10 +267,16 @@ class calendar_sif extends calendar_boupdate unset($event['alarm']); } - if($_calID > 0) - { + if($_calID > 0) { // update entry $event['id'] = $_calID; + } else { + if (isset($event['whole_day']) && $event['whole_day'] + && isset ($deviceInfo) && is_array($deviceInfo) + && isset($deviceInfo['nonBlockingAllday']) + && $deviceInfo['nonBlockingAllday']) { + $event['non_blocking'] = '1'; + } } if($eventID = $this->update($event, TRUE)) { @@ -308,14 +299,15 @@ class calendar_sif extends calendar_boupdate } /** - * return a vcard + * return a sife * - * @param int $_id the id of the contact - * @param int $_vcardProfile profile id for mapping from vcard values to egw addressbook + * @param int $_id the id of the event * @return string containing the vcard */ function getSIF($_id) { + $sysCharSet = $GLOBALS['egw']->translation->charset(); + $fields = array_unique(array_values($this->sifMapping)); sort($fields); @@ -323,11 +315,17 @@ class calendar_sif extends calendar_boupdate #error_log("FOUND EVENT: ". print_r($event, true)); if($event = $this->read($_id,null,false,'server')) { - $sysCharSet = $GLOBALS['egw']->translation->charset(); - $vcal = new Horde_iCalendar; + + if ($this->uidExtension) { + if (!preg_match('/\[UID:.+\]/m', $event['description'])) { + $event['description'] .= "\n[UID:" . $event['uid'] . "]"; + } + } + + $vcal = &new Horde_iCalendar('1.0'); - $sifEvent = ''; + $sifEvent = self::xml_decl . "\n" . self::SIF_decl; foreach($this->sifMapping as $sifField => $egwField) { @@ -340,14 +338,6 @@ class calendar_sif extends calendar_boupdate switch($sifField) { - case 'Categories': - if(!empty($value)) { - $value = implode('; ', $this->get_categories(explode(',',$value))); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - $sifEvent .= "<$sifField>$value"; - break; - case 'Importance': $value = $value-1; $sifEvent .= "<$sifField>$value"; @@ -437,6 +427,48 @@ class calendar_sif extends calendar_boupdate $sifEvent .= ''. $occurrences .''; } break; + case MCAL_RECUR_MONTHLY_MDAY: + $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); + $recurStartDate = mktime(0,0,0,date('m',$event['start']), date('d', $event['start']), date('Y', $event['start'])); + + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursMonthly .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. $vcal->_exportDateTime($recurStartDate) .''; + if($event['recur_enddate'] == 0) { + $sifEvent .= '1'; + } else { + $recurEndDate = mktime(24, 0, 0, date('m',$event['recur_enddate']), date('d', $event['recur_enddate']), date('Y', $event['recur_enddate'])); + + $sifEvent .= '0'; + $sifEvent .= ''. $vcal->_exportDateTime($recurEndDate) .''; + } + break; + case MCAL_RECUR_MONTHLY_WDAY: + $weekMaskMap = array('Sun' => self::olSunday, 'Mon' => self::olMonday, 'Tue' => self::olTuesday, + 'Wed' => self::olWednesday, 'Thu' => self::olThursday, 'Fri' => self::olFriday, + 'Sat' => self::olSaturday); + $eventInterval = ($event['recur_interval'] > 1 ? $event['recur_interval'] : 1); + $recurStartDate = mktime(0,0,0,date('m',$event['start']), date('d', $event['start']), date('Y', $event['start'])); + + $sifEvent .= "<$sifField>1"; + $sifEvent .= ''. self::olRecursMonthNth .''; + $sifEvent .= ''. $eventInterval .''; + $sifEvent .= ''. $vcal->_exportDateTime($recurStartDate) .''; + $sifEvent .= '' . (1 + (int) ((date('d',$event['start'])-1) / 7)) . ''; + if($event['recur_enddate'] == 0) { + $sifEvent .= '1'; + $sifEvent .= '' . $weekMaskMap[date('D',$event['start'])] . ''; + } else { + $recurEndDate = mktime(24, 0, 0, date('m',$event['recur_enddate']), date('d', $event['recur_enddate']), date('Y', $event['recur_enddate'])); + + $sifEvent .= '0'; + $sifEvent .= ''. $vcal->_exportDateTime($recurEndDate) .''; + $sifEvent .= '' . $weekMaskMap[date('D',$event['start'])] . ''; + } + break; + case MCAL_RECUR_YEARLY: + break; } break; @@ -456,9 +488,10 @@ class calendar_sif extends calendar_boupdate break; case 'Start': - if($event['end'] - $event['start'] == 86399 && date('Y-m-d', $event['end']) == date('Y-m-d', $event['start'])) { + if ($this->isWholeDay($event)) { $value = date('Y-m-d', $event['start']); $sifEvent .= "$value"; + $vaule = date('Y-m-d', $event['end']); $sifEvent .= "$value"; $sifEvent .= "1"; } else { @@ -487,14 +520,23 @@ class calendar_sif extends calendar_boupdate } break; + case 'Categories': + if(!empty($value)) { + $value = implode('; ', $this->get_categories(explode(',',$value))); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } else { + break; + } + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); $sifEvent .= "<$sifField>$value"; break; } } $sifEvent .= ''; - return base64_encode($sifEvent); + return $sifEvent; } if($this->xmlrpc) @@ -504,4 +546,31 @@ class calendar_sif extends calendar_boupdate return False; } + /** + * Set the supported fields + * + * Currently we only store name and version, manucfacturer is always Funambol + * + * @param string $_productName + * @param string $_productSoftwareVersion + */ + function setSupportedFields($_productName='', $_productSoftwareVersion='') + { + $state = &$_SESSION['SyncML.state']; + $deviceInfo = $state->getClientDeviceInfo(); + + if(isset($deviceInfo) && is_array($deviceInfo)) { + if(isset($deviceInfo['uidExtension']) && + $deviceInfo['uidExtension']){ + $this->uidExtension = true; + } + } + // store product name and version, to be able to use it elsewhere + if ($_productName) { + $this->productName = strtolower($_productName); + if (preg_match('/^[^\d]*(\d+\.?\d*)[\.|\d]*$/', $_productSoftwareVersion, $matches)) { + $this->productSoftwareVersion = $matches[1]; + } + } + } } diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index 8d07d90ce9..069e1cfeb3 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -5,7 +5,8 @@ * @link http://www.egroupware.org * @package calendar * @author Ralf Becker - * @copyright (c) 2005-9 by RalfBecker-At-outdoor-training.de + * @author Joerg Lehrke + * @copyright (c) 2005-8 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ @@ -105,12 +106,18 @@ class calendar_so * * All times (start, end and modified) are returned as timesstamps in servertime! * - * @param int|array/string $ids id or array of id's of the entries to read, or string with a single uid + * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid * @param int $recur_date=0 if set read the next recurrance at or after the timestamp, default 0 = read the initital one * @return array|boolean array with id => data pairs or false if entry not found */ function read($ids,$recur_date=0) { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } else { + $minimum_uid_length = 8; + } + //echo "

socal::read(".print_r($ids,true).",$recur_date)
\n".function_backtrace()."

\n"; $table_def = $this->db->get_table_definitions('calendar',$this->cal_table); @@ -131,7 +138,9 @@ class calendar_so } else { + // We only what the parents to match $where['cal_uid'] = $ids; + $where['cal_reference'] = 0; } if ((int) $recur_date) { @@ -153,6 +162,15 @@ class calendar_so } if (!$events) return false; + foreach ($events as $event) { + if (!isset($event['uid']) || strlen($event['uid']) < $minimum_uid_length) { + // event (without uid), not strong enough uid => create new uid + $event['uid'] = $GLOBALS['egw']->common->generate_uid('calendar',$event['id']); + $this->db->update($this->cal_table, array('cal_uid' => $event['uid']), + array('cal_id' => $event['id']),__LINE__,__FILE__,'calendar'); + } + } + // check if we have a real recurance, if not set $recur_date=0 if (is_array($ids) || $events[(int)$ids]['recur_type'] == MCAL_RECUR_NONE) { @@ -222,7 +240,7 @@ class calendar_so $sql = ''; if ($cat_id) { - if (!is_array($cat_ids) && !@$GLOBALS['egw_info']['user']['preferences']['common']['cats_no_subs']) + if (!is_array($cat_id) && !@$GLOBALS['egw_info']['user']['preferences']['common']['cats_no_subs']) { $cats = $GLOBALS['egw']->categories->return_all_children($cat_id); } @@ -260,12 +278,11 @@ class calendar_so * @param string|array $_cols=null what to select, default "$this->repeats_table.*,$this->cal_table.*,cal_start,cal_end,cal_recur_date" * if specified an iterator for the rows is returned * @param string $append='' SQL to append to the query before $order, eg. for a GROUP BY clause - * @return array|boolean|iterator of cal_id => event pairs, or false if error in the parameters, or iterator if !is_null($_cols) + * @return array of cal_ids, or false if error in the parameters * * ToDo: search custom-fields too */ - function &search($start,$end,$users,$cat_id=0,$filter='',$query='',$offset=False,$num_rows=0,$order = 'cal_start',$show_rejected=true, - $_cols=null,$append='') + function &search($start,$end,$users,$cat_id=0,$filter='',$query='',$offset=False,$num_rows=0,$order='cal_start',$show_rejected=true,$_cols=null,$append='') { //echo '

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

\n"; @@ -344,6 +361,7 @@ class calendar_so $selects[0]['cols'] = $selects[1]['cols'] = $select['cols']; // restore the original cols } + // error_log("calendar_so_search:\n" . print_r($selects, true)); $rs = $this->db->union($selects,__LINE__,__FILE__,$order,$offset,$num_rows); } else // MsSQL oder MySQL 3.23 @@ -487,6 +505,12 @@ ORDER BY cal_user_type, cal_usre_id */ function save($event,&$set_recurrences,$change_since=0,&$etag=null) { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } else { + $minimum_uid_length = 8; + } + //echo "

socal::save(,$change_since) event="; _debug_array($event); //error_log(__METHOD__."(".str_replace(array("\n",' '),'',print_r($event,true)).",$set_recurrences,$change_since,$etag)"); @@ -520,9 +544,17 @@ ORDER BY cal_user_type, cal_usre_id return 0; // wrong etag, someone else updated the entry } if (!is_null($etag)) ++$etag; + // events need to have at least one participant, default to the owner + if (!isset($event['cal_participants'])) { + $event['cal_participants'] = array($event['cal_owner'] => 'A'); + } + if (!isset($event['cal_participants'][$event['cal_owner']])) { + $event['cal_participants'][$event['cal_owner']] = 'A'; + } } else { + // new event if (!$event['cal_owner']) $event['cal_owner'] = $GLOBALS['egw_info']['user']['account_id']; if (!$event['cal_id'] && !isset($event['cal_uid'])) $event['cal_uid'] = ''; // uid is NOT NULL! @@ -532,45 +564,54 @@ ORDER BY cal_user_type, cal_usre_id { return false; } - // new event (without uid), not strong enough uid or new created referencing event => create new uid - if (strlen($event['cal_uid']) < 20 || is_numeric($event['cal_uid']) || - $event['cal_reference'] && strpos($event['cal_uid'],'cal-'.$event['calreference'].'-') !== false) - { - $event['cal_uid'] = $GLOBALS['egw']->common->generate_uid('calendar',$cal_id); - $this->db->update($this->cal_table,array('cal_uid' => $event['cal_uid']),array('cal_id' => $cal_id),__LINE__,__FILE__,'calendar'); - } $etag = 0; // new events need to have at least one participant, default to the owner - if (!isset($event['cal_participants'])) - { + if (!isset($event['cal_participants'])) { $event['cal_participants'] = array($event['cal_owner'] => 'A'); } + if (!isset($event['cal_participants'][$event['cal_owner']])) { + $event['cal_participants'][$event['cal_owner']] = 'A'; + } + } + if (!isset($event['cal_uid']) || strlen($event['cal_uid']) < $minimum_uid_length + || ($event['cal_reference'] && strpos($event['cal_uid'], + 'cal-'.$event['calreference'].'-') !== false)) { + // event (without uid), not strong enough uid + // or new created referencing event => create new uid + $event['cal_uid'] = $GLOBALS['egw']->common->generate_uid('calendar',$cal_id); + $this->db->update($this->cal_table, array('cal_uid' => $event['cal_uid']), + array('cal_id' => $event['cal_id']),__LINE__,__FILE__,'calendar'); } // write information about recuring event, if recur_type is present in the array if (isset($event['recur_type'])) { + // save the original start + $min = (int) $this->db->select($this->dates_table,'MIN(cal_start)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchSingle(); + if (isset($event['recur_exception']) && is_array($event['recur_exception']) && count($event['recur_exception'])) { - // delete execeptions from the user and dates table, it could be the first time + // delete the execeptions from the user and dates table, it could be the first time $this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date' => $event['recur_exception']),__LINE__,__FILE__,'calendar'); $this->db->delete($this->dates_table,array('cal_id' => $cal_id,'cal_start' => $event['recur_exception']),__LINE__,__FILE__,'calendar'); - - $event['recur_exception'] = implode(',',$event['recur_exception']); } else { - $event['recur_exception'] = null; + $event['recur_exception'] = array(); } if (!$set_recurrences) { - // check if the recure-information changed + // check if the recurrence-information changed $old_recur = $this->db->select($this->repeats_table,'*',array('cal_id' => $cal_id),__LINE__,__FILE__,false,'','calendar')->fetch(); $old_exceptions = $old_recur['recur_exception'] ? explode(',',$old_recur['recur_exception']) : array(); - $exceptions = $event['recur_exception'] ? explode(',',$event['recur_exception']) : array(); - $set_recurrences = $event['recur_type'] != $old_recur['recur_type'] || $event['recur_data'] != $old_recur['recur_data'] || - $event['recur_interval'] != $old_recur['recur_interval'] || $event['recur_enddate'] != $old_recur['recur_enddate'] || - count(array_diff($old_exceptions,$exceptions)); // exception deleted or added + $set_recurrences = (isset($event['cal_start']) && (int)$min != (int) $event['cal_start']) || + $event['recur_type'] != $old_recur['recur_type'] || $event['recur_data'] != $old_recur['recur_data'] || + (int)$event['recur_interval'] != (int)$old_recur['recur_interval'] || $event['recur_enddate'] != $old_recur['recur_enddate'] || + count(array_diff($old_exceptions,$event['recur_exception'])); // exception deleted or added + $max = (int) $this->db->select($this->dates_table,'MAX(cal_start)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchSingle(); + } else { + $max = 0; } + $event['recur_exception'] = empty($event['recur_exception']) ? null : implode(',',$event['recur_exception']); unset($event[0]); // unset the 'etag=etag+1', as it's not in the repeats table if($event['recur_type'] != MCAL_RECUR_NONE) { @@ -582,17 +623,21 @@ ORDER BY cal_user_type, cal_usre_id } if ($set_recurrences) { + if ((int)$min != (int)$event['cal_start']) + { + // We need to reset all recurrences + $max = -1; + } // delete all, but the lowest dates record - $min = (int) $this->db->select($this->dates_table,'MIN(cal_start)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn(); - $this->db->delete($this->dates_table,array( 'cal_id' => $cal_id, 'cal_start > '.(int)$min, ),__LINE__,__FILE__,'calendar'); - // delete all user-records, with recur-date != 0 + + // delete all user-records, with recur-date > old enddate $this->db->delete($this->user_table,array( 'cal_id' => $cal_id, - 'cal_recur_date != 0', + 'cal_recur_date > '.(int)$max, ),__LINE__,__FILE__,'calendar'); } } @@ -647,10 +692,10 @@ ORDER BY cal_user_type, cal_usre_id } //pgoerzen: don't add an alarm if it is before the current date. - if ($event['recur_type'] && ($tmp_event = $this->read($eventID, time() + $alarm['offset']))) + /*if ($event['recur_type'] && ($tmp_event = $this->read($eventID, time() + $alarm['offset']))) { $alarm['time'] = $tmp_event['cal_start'] - $alarm['offset']; - } + } */ $this->save_alarm($cal_id,$alarm); } @@ -708,7 +753,7 @@ ORDER BY cal_user_type, cal_usre_id { // move the recur-date of the participants $this->db->query("UPDATE $this->user_table SET cal_recur_date=cal_recur_date+$move_start WHERE $where AND cal_recur_date ". - ((int)$change_since ? '>= '.(int)$change_since : '!= 0'),__LINE__,__FILE__); + ((int)$change_since ? '>= '.(int)$change_since : '!= 0') + " ORDER BY cal_recur_date DESC",__LINE__,__FILE__); } if ($move_start || $move_end) { @@ -762,7 +807,6 @@ ORDER BY cal_user_type, cal_usre_id * @param int $cal_id * @param array $participants id => status pairs * @param int|boolean $change_since=0 false=new entry, > 0 time from which on the repetitions should be changed, default 0=all - * @param int $recur_date=0 time of which repetitions should be updated, default 0=all * @return int|boolean number of updated recurrences or false on error */ function participants($cal_id,$participants,$change_since=0) @@ -786,6 +830,7 @@ ORDER BY cal_user_type, cal_usre_id if ($change_since !== false) // existing entries only { // delete not longer set participants + $where[0] = 'cal_recur_date=0'; $deleted = array(); foreach($this->db->select($this->user_table,'DISTINCT cal_user_type,cal_user_id,cal_quantity',$where, __LINE__,__FILE__,false,'','calendar') as $row) @@ -795,11 +840,15 @@ ORDER BY cal_user_type, cal_usre_id { $deleted[$row['cal_user_type']][] = $row['cal_user_id']; } - elseif($row['cal_quantity'] == (substr($participants[$uid],1) ? substr($participants[$uid],1) : 1)) + elseif((($row['cal_user_type'] == 'r') && + // quantity of resource unchanged + ($row['cal_quantity'] == (substr($participants[$uid],1) ? substr($participants[$uid],1) : 1))) || + ($row['cal_status'] == $participants[$uid])) { - unset($participants[$uid]); // we dont touch them + unset($participants[$uid]); // we don't touch them } } + unset($where[0]); if (count($deleted)) { @@ -815,21 +864,32 @@ ORDER BY cal_user_type, cal_usre_id $this->db->delete($this->user_table,$where + array('('.implode(' OR ',$to_or).')'),__LINE__,__FILE__,'calendar'); } } - if (count($participants)) // these are NEW participants now + if (count($participants)) // these are ALL participants now - we exclude the existing ones later { // find all recurrences, as they all need the new parts to be added $recurrences = array(); + $existing_participants = array(); + if ($change_since !== false) // existing entries only { foreach($this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__,false,'','calendar') as $row) { $recurrences[] = $row['cal_recur_date']; } + + // existing participants must not be updated + foreach($this->db->select($this->user_table,'DISTINCT cal_user_type,cal_user_id',$where,__LINE__,__FILE__,false,'','calendar') as $row) + { + $existing_participants[] = $this->combine_user($row['cal_user_type'],$row['cal_user_id']); + } } if (!count($recurrences)) $recurrences[] = 0; // insert the default one foreach($participants as $uid => $status) { + if (in_array($uid,$existing_participants)) continue; // don't update existing participants + + $id = null; $this->split_user($uid,$type,$id); foreach($recurrences as $recur_date) { @@ -854,7 +914,7 @@ ORDER BY cal_user_type, cal_usre_id * @param int $cal_id * @param char $user_type 'u' regular user, 'r' resource, 'c' contact * @param int $user_id - * @param int|string $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A' + * @param int|char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A' * @param int $recur_date=0 date to change, or 0 = all since now * @return int number of changed recurrences */ @@ -897,7 +957,7 @@ ORDER BY cal_user_type, cal_usre_id ),$where,__LINE__,__FILE__,'calendar'); } $ret = $this->db->affected_rows(); - error_log(__METHOD__."($cal_id,$user_type,$user_id,$status,$recur_date) = $ret"); + //error_log(__METHOD__."($cal_id,$user_type,$user_id,$status,$recur_date) = $ret"); return $this->db->affected_rows(); } @@ -923,6 +983,8 @@ ORDER BY cal_user_type, cal_usre_id { if ($status == 'G') continue; // dont save group-invitations + $type = ''; + $id = null; $this->split_user($uid,$type,$id); $this->db->insert($this->user_table,array( 'cal_status' => $status !== true ? $status[0] : 'U', @@ -1024,9 +1086,10 @@ ORDER BY cal_user_type, cal_usre_id * * @param int $cal_id Id of the calendar-entry * @param array $alarm array with fields: text, owner, enabled, .. + * @param timestamp $now_su=0 timestamp for modification of related event * @return string id of the alarm */ - function save_alarm($cal_id,$alarm) + function save_alarm($cal_id, $alarm, $now_su = 0) { //echo "

save_alarm(cal_id=$cal_id, alarm="; print_r($alarm); echo ")

\n"; if (!($id = $alarm['id'])) @@ -1050,6 +1113,13 @@ ORDER BY cal_user_type, cal_usre_id { return False; } + + // update the modification information of the related event + $datetime = $GLOBALS['egw']->datetime; + $now = ($now_su ? $now_su : time() + $datetime->this->tz_offset); + $modifier = $GLOBALS['egw_info']['user']['account_id']; + $this->db->update($this->cal_table, array('cal_modified' => $now, 'cal_modifier' => $modifier), + array('cal_id' => $cal_id), __LINE__, __FILE__, 'calendar'); return $id; } @@ -1074,10 +1144,20 @@ ORDER BY cal_user_type, cal_usre_id * delete one alarms identified by its id * * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too + * @param timestamp $now_su=0 timestamp for modification of related event * @return int number of alarms deleted */ - function delete_alarm($id) + function delete_alarm($id, $now_su = 0) { + // update the modification information of the related event + list(,$cal_id) = explode(':',$id); + if ($cal_id) { + $datetime = $GLOBALS['egw']->datetime; + $now = ($now_su ? $now_su : time() + $datetime->this->tz_offset); + $modifier = $GLOBALS['egw_info']['user']['account_id']; + $this->db->update($this->cal_table, array('cal_modified' => $now, 'cal_modifier' => $modifier), + array('cal_id' => $cal_id), __LINE__, __FILE__, 'calendar'); + } return $this->async->cancel_timer($id); } @@ -1087,7 +1167,7 @@ ORDER BY cal_user_type, cal_usre_id * @param array|int $old_user integer old user or array with keys 'account_id' and 'new_owner' as the deleteaccount hook uses it * @param int $new_user=null */ - function deleteaccount($data) + function deleteaccount($old_user, $newuser=null) { if (is_array($old_user)) { @@ -1096,6 +1176,8 @@ ORDER BY cal_user_type, cal_usre_id } if (!(int)$new_user) { + $user_type = ''; + $user_id = null; $this->split_user($old_user,$user_type,$user_id); if ($user_type == 'u') // only accounts can be owners of events @@ -1144,4 +1226,134 @@ ORDER BY cal_user_type, cal_usre_id ),__LINE__,__FILE__,'calendar'); } } + + /** + * get stati of all recurrences of an event for a specific participant + * + * @param int $cal_id + * @param int $uid participant uid + * @return array recur_date => status pairs (index 0 => main status) + */ + function get_recurrences($cal_id, $uid) + { + $user_type = $user_id = null; + $this->split_user($uid, $user_type, $user_id); + $participant_status = array(); + $where = array('cal_id' => $cal_id); + foreach($this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__,false,'','calendar') as $row) + { + // inititalize the array + $participant_status[$row['cal_recur_date']] = null; + } + $where = array( + 'cal_id' => $cal_id, + 'cal_user_type' => $user_type ? $user_type : 'u', + 'cal_user_id' => $user_id, + ); + foreach ($this->db->select($this->user_table,'cal_recur_date,cal_status',$where, + __LINE__,__FILE__,false,'','calendar') as $row) + { + $participant_status[$row['cal_recur_date']] = $row['cal_status']; + } + return $participant_status; + } + + /** + * get all participants of an event + * + * @param int $cal_id + * @param int $recur_date=0 gives participants of this recurrence, default 0=all + * + * @return array participants + */ + function get_participants($cal_id, $recur_date=0) + { + $participants = array(); + $where = array('cal_id' => $cal_id); + if ($recur_date) + { + $where['cal_recur_date'] = $recur_date; + } + + foreach ($this->db->select($this->user_table,'DISTINCT cal_user_type,cal_user_id', $where, + __LINE__,__FILE__,false,'','calendar') as $row) + { + $uid = $this->combine_user($row['cal_user_type'], $row['cal_user_id']); + $id = $row['cal_user_type'] . $row['cal_user_id']; + $participants[$id]['type'] = $row['cal_user_type']; + $participants[$id]['id'] = $row['cal_user_id']; + $participants[$id]['uid'] = $uid; + } + return $participants; + } + + /** + * get all releated events + * + * @param int $uid UID of the series + * + * @return array of event exception ids for all events which share $uid + */ + function get_related($uid) + { + $where = array( + 'cal_uid' => $uid, + ); + $related = array(); + foreach ($this->db->select($this->cal_table,'cal_id,cal_reference',$where, + __LINE__,__FILE__,false,'','calendar') as $row) + { + if ($row['cal_reference']) + { + // not the series entry itself + $related[$row['cal_id']] = $row['cal_reference']; + } + } + return $related; + } + + /** + * Gets the exception days of a given recurring event caused by + * irregular participant stati + * + * @param array $event Recurring Event. + * + * @return array Array of exception days (false for non-recurring events). + */ + function get_recurrence_exceptions(&$event) + { + $cal_id = (int) $event['id']; + if (!$cal_id || $event['recur_type'] == MCAL_RECUR_NONE) return false; + + $days = array(); + + $participants = $this->get_participants($event['id'], 0); + + // Check if the stati for all participants are identical for all recurrences + foreach ($participants as $uid => $attendee) + { + switch ($attendee['type']) + { + case 'u': // account + case 'c': // contact + case 'e': // email address + $recurrences = $this->get_recurrences($event['id'], $uid); + foreach ($recurrences as $recur_date => $recur_status) + { + if ($recur_date && $recur_status != $recurrences[0]) + { + // Every distinct status results in an exception + $days[] = $recur_date; + } + } + break; + default: // We don't handle the rest + break; + } + } + $days = array_unique($days); + sort($days); + return $days; + } + }