From b23bd253239f1022d45c859eb61574b057e3602b Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sat, 18 Sep 2010 12:34:58 +0000 Subject: [PATCH] * SyncML performance patches for calendar datastore (merged r32053) --- calendar/inc/class.calendar_boupdate.inc.php | 2201 +++++++++++++++++ phpgwapi/inc/class.contenthistory.inc.php | 36 +- phpgwapi/inc/horde/Horde/SyncML/State_egw.php | 740 ++++++ 3 files changed, 2966 insertions(+), 11 deletions(-) create mode 100644 calendar/inc/class.calendar_boupdate.inc.php create mode 100644 phpgwapi/inc/horde/Horde/SyncML/State_egw.php diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php new file mode 100644 index 0000000000..bc8922b740 --- /dev/null +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -0,0 +1,2201 @@ + + * @author Joerg Lehrke + * @copyright (c) 2005-9 by RalfBecker-At-outdoor-training.de + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +// types of messsages send by calendar_boupdate::send_update +define('MSG_DELETED',0); +define('MSG_MODIFIED',1); +define('MSG_ADDED',2); +define('MSG_REJECTED',3); +define('MSG_TENTATIVE',4); +define('MSG_ACCEPTED',5); +define('MSG_ALARM',6); +define('MSG_DISINVITE',7); +define('MSG_DELEGATED',8); + +/** + * Class to access AND manipulate all calendar data (business object) + * + * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate: + * UI only operates in user-time, so there have to be no conversation at all !!! + * BO's functions take and return user-time only (!), they convert internaly everything to servertime, because + * SO operates only on server-time + * + * As this BO class deals with dates/times of several types and timezone, each variable should have a postfix + * appended, telling with type it is: _s = seconds, _su = secs in user-time, _ss = secs in server-time, _h = hours + * + * All new BO code (should be true for eGW in general) NEVER use any $_REQUEST ($_POST or $_GET) vars itself. + * Nor does it store the state of any UI-elements (eg. cat-id selectbox). All this is the task of the UI class(es) !!! + * + * All permanent debug messages of the calendar-code should done via the debug-message method of the bocal class !!! + */ + +class calendar_boupdate extends calendar_bo +{ + /** + * Category ACL allowing to add a given category + */ + const CAT_ACL_ADD = 512; + /** + * Category ACL allowing to change status of a participant + */ + const CAT_ACL_STATUS = 1024; + + /** + * name of method to debug or level of debug-messages: + * False=Off as higher as more messages you get ;-) + * 1 = function-calls incl. parameters to general functions like search, read, write, delete + * 2 = function-calls to exported helper-functions like check_perms + * 4 = function-calls to exported conversation-functions like date2ts, date2array, ... + * 5 = function-calls to private functions + * @var mixed + */ + var $debug; + + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + var $logfile = '/tmp/log-calendar-boupdate'; + + /** + * Cached timezone data + * + * @var array id => data + */ + protected static $tz_cache = array(); + + /** + * Constructor + */ + function __construct() + { + if ($this->debug > 0) $this->debug_message('calendar_boupdate::__construct() started',True); + + parent::__construct(); // calling the parent constructor + + if ($this->debug > 0) $this->debug_message('calendar_boupdate::__construct() finished',True); + } + + /** + * updates or creates an event, it (optionaly) checks for conflicts and sends the necessary notifications + * + * @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=false should we ignore the acl + * @param boolean $updateTS=true update the content history of the event + * @param array &$messages=null messages about because of missing ACL removed participants or categories + * @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)! + */ + function update(&$event,$ignore_conflicts=false,$touch_modified=true,$ignore_acl=false,$updateTS=true,&$messages=null) + { + //error_log(__METHOD__."(".array2string($event).",$ignore_conflicts,$touch_modified,$ignore_acl)"); + if ($this->debug > 1 || $this->debug == 'update') + { + $this->debug_message('calendar_boupdate::update(%1,ignore_conflict=%2,touch_modified=%3,ignore_acl=%4)', + false,$event,$ignore_conflicts,$touch_modified,$ignore_acl); + } + // check some minimum requirements: + // - new events need start, end and title + // - updated events cant set start, end or title to empty + if (!$event['id'] && (!$event['start'] || !$event['end'] || !$event['title']) || + $event['id'] && (isset($event['start']) && !$event['start'] || isset($event['end']) && !$event['end'] || + isset($event['title']) && !$event['title'])) + { + return false; + } + + if (($new_event = !$event['id'])) // some defaults for new entries + { + // if no owner given, set user to owner + if (!$event['owner']) $event['owner'] = $this->user; + // set owner as participant if none is given + if (!is_array($event['participants']) || !count($event['participants'])) + { + $status = $event['owner'] == $this->user ? 'A' : 'U'; + $status = calendar_so::combine_status($status, 1, 'CHAIR'); + $event['participants'] = array($event['owner'] => $status); + } + } + + // check if user has the permission to update / create the event + if (!$ignore_acl && (!$new_event && !$this->check_perms(EGW_ACL_EDIT,$event['id']) || + $new_event && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner'])) && + !$this->check_perms(EGW_ACL_ADD,0,$event['owner'])) + { + return false; + } + if ($new_event) + { + $event['created'] = $this->now_su; + $event['creator'] = $GLOBALS['egw_info']['user']['account_id']; + $old_event = array(); + } + else + { + $old_event = $this->read((int)$event['id'],null,$ignore_acl); + } + + // do we need to check, if user is allowed to invite the invited participants + if ($this->require_acl_invite && ($removed = $this->remove_no_acl_invite($event,$old_event))) + { + // report removed participants back to user + foreach($removed as $key => $account_id) + { + $removed[$key] = $this->participant_name($account_id); + } + $messages[] = lang('%1 participants removed because of missing invite grants',count($removed)). + ': '.implode(', ',$removed); + } + // check category based ACL + if ($event['category']) + { + if (!is_array($event['category'])) $event['category'] = explode(',',$event['category']); + if (!$old_event || !isset($old_event['category'])) + { + $old_event['category'] = array(); + } + elseif (!is_array($old_event['category'])) + { + $old_event['category'] = explode(',', $old_event['category']); + } + foreach($event['category'] as $key => $cat_id) + { + // check if user is allowed to update event categories + if ((!$old_event || !in_array($cat_id,$old_event['category'])) && + self::has_cat_right(self::CAT_ACL_ADD,$cat_id,$this->user) === false) + { + unset($event['category'][$key]); + // report removed category to user + $removed_cats[$cat_id] = $this->categories->id2name($cat_id); + continue; // no further check, as cat was removed + } + // for new or moved events check status of participants, if no category status right --> set all status to 'U' = unknown + if (!$status_reset_to_unknown && + self::has_cat_right(self::CAT_ACL_STATUS,$cat_id,$this->user) === false && + (!$old_event || $old_event['start'] != $event['start'] || $old_event['end'] != $event['end'])) + { + foreach((array)$event['participants'] as $uid => $status) + { + calendar_so::split_status($status,$q,$r); + if ($status != 'U') + { + $event['participants'][$uid] = calendar_so::combine_status('U',$q,$r); + // todo: report reset status to user + } + } + $status_reset_to_unknown = true; // once is enough + } + } + if ($removed_cats) + { + $messages[] = lang('Category %1 removed because of missing rights',implode(', ',$removed_cats)); + } + if ($status_reset_to_unknown) + { + $messages[] = lang('Status of participants set to unknown because of missing category rights'); + } + } + // check for conflicts only happens !$ignore_conflicts AND if start + end date are given + if (!$ignore_conflicts && !$event['non_blocking'] && isset($event['start']) && isset($event['end'])) + { + $types_with_quantity = array(); + foreach($this->resources as $type => $data) + { + if ($data['max_quantity']) $types_with_quantity[] = $type; + } + // get all NOT rejected participants and evtl. their quantity + $quantity = $users = array(); + foreach($event['participants'] as $uid => $status) + { + calendar_so::split_status($status,$q,$r); + if ($status[0] == 'R') continue; // ignore rejected participants + + if ($uid < 0) // group, check it's members too + { + $users += $GLOBALS['egw']->accounts->members($uid,true); + $users = array_unique($users); + } + $users[] = $uid; + if (in_array($uid[0],$types_with_quantity)) + { + $quantity[$uid] = $q; + } + } + $overlapping_events =& $this->search(array( + 'start' => $event['start'], + 'end' => $event['end'], + 'users' => $users, + 'ignore_acl' => true, // otherwise we get only events readable by the user + 'enum_groups' => true, // otherwise group-events would not block time + )); + if ($this->debug > 2 || $this->debug == 'update') + { + $this->debug_message('calendar_boupdate::update() checking for potential overlapping events for users %1 from %2 to %3',false,$users,$event['start'],$event['end']); + } + $max_quantity = $possible_quantity_conflicts = $conflicts = array(); + foreach((array) $overlapping_events as $k => $overlap) + { + if ($overlap['id'] == $event['id'] || // that's the event itself + $overlap['id'] == $event['reference'] || // event is an exception of overlap + $overlap['non_blocking']) // that's a non_blocking event + { + continue; + } + if ($this->debug > 3 || $this->debug == 'update') + { + $this->debug_message('calendar_boupdate::update() checking overlapping event %1',false,$overlap); + } + // check if the overlap is with a rejected participant or within the allowed quantity + $common_parts = array_intersect($users,array_keys($overlap['participants'])); + foreach($common_parts as $n => $uid) + { + if ($overlap['participants'][$uid][0] == 'R') + { + unset($common_parts[$n]); + continue; + } + if (is_numeric($uid) || !in_array($uid[0],$types_with_quantity)) + { + continue; // no quantity check: quantity allways 1 ==> conflict + } + if (!isset($max_quantity[$uid])) + { + $res_info = $this->resource_info($uid); + $max_quantity[$uid] = $res_info[$this->resources[$uid[0]]['max_quantity']]; + } + $quantity[$uid] += max(1,(int) substr($overlap['participants'][$uid],2)); + if ($quantity[$uid] <= $max_quantity[$uid]) + { + $possible_quantity_conflicts[$uid][] =& $overlapping_events[$k]; // an other event can give the conflict + unset($common_parts[$n]); + continue; + } + // now we have a quantity conflict for $uid + } + if (count($common_parts)) + { + if ($this->debug > 3 || $this->debug == 'update') + { + $this->debug_message('calendar_boupdate::update() conflicts with the following participants found %1',false,$common_parts); + } + $conflicts[$overlap['id'].'-'.$this->date2ts($overlap['start'])] =& $overlapping_events[$k]; + } + } + // check if we are withing the allowed quantity and if not add all events using that resource + // seems this function is doing very strange things, it gives empty conflicts + foreach($max_quantity as $uid => $max) + { + if ($quantity[$uid] > $max) + { + foreach((array)$possible_quantity_conflicts[$uid] as $conflict) + { + $conflicts[$conflict['id'].'-'.$this->date2ts($conflict['start'])] =& $possible_quantity_conflicts[$k]; + } + } + } + unset($possible_quantity_conflicts); + + if (count($conflicts)) + { + foreach($conflicts as $key => $conflict) + { + $conflict['participants'] = array_intersect_key($conflict['participants'],$event['participants']); + if (!$this->check_perms(EGW_ACL_READ,$conflict)) + { + $conflicts[$key] = array( + 'id' => $conflict['id'], + 'title' => lang('busy'), + 'participants' => $conflict['participants'], + 'start' => $conflict['start'], + 'end' => $conflict['end'], + ); + } + } + if ($this->debug > 2 || $this->debug == 'update') + { + $this->debug_message('calendar_boupdate::update() %1 conflicts found %2',false,count($conflicts),$conflicts); + } + return $conflicts; + } + } + + // save the event to the database + if ($touch_modified) + { + $event['modified'] = $this->now_su; // we are still in user-time + $event['modifier'] = $GLOBALS['egw_info']['user']['account_id']; + } + //echo "saving $event[id]="; _debug_array($event); + $event2save = $event; + + if (!($cal_id = $this->save($event, $ignore_acl, $updateTS))) + { + return $cal_id; + } + + $event = $this->read($cal_id); // we re-read the event, in case only partial information was update and we need the full info for the notifies + //echo "new $cal_id="; _debug_array($event); + + if ($this->log_file) + { + $this->log2file($event2save,$event,$old_event); + } + // send notifications + if ($new_event) + { + $this->send_update(MSG_ADDED,$event['participants'],'',$event); + } + else // update existing event + { + $this->check4update($event,$old_event); + } + // notify the link-class about the update, as other apps may be subscribt to it + egw_link::notify_update('calendar',$cal_id,$event); + + return $cal_id; + } + + /** + * Remove participants current user has no right to invite + * + * @param array &$event new event + * @param array $old_event=null old event with already invited participants + * @return array removed participants because of missing invite grants + */ + public function remove_no_acl_invite(array &$event,array $old_event=null) + { + if (!$this->require_acl_invite) + { + return array(); // nothing to check, everyone can invite everyone else + } + if ($event['id'] && is_null($old_event)) + { + $old_event = $this->read($event['id']); + } + $removed = array(); + foreach($event['participants'] as $uid => $status) + { + if ((is_null($old_event) || !isset($old_event['participants'][$uid])) && !$this->check_acl_invite($uid)) + { + unset($event['participants'][$uid]); // remove participant + $removed[] = $uid; + } + } + //echo "

".__METHOD__."($event[title],".($old_event?'$old_event':'NULL').") returning ".array2string($removed)."

"; + return $removed; + } + + /** + * Check if current user is allowed to invite a given participant + * + * @param int|string $uid + * @return boolean + */ + public function check_acl_invite($uid) + { + if (!is_numeric($uid)) return true; // nothing implemented for resources so far + + if (!$this->require_acl_invite) + { + $ret = true; // no grant required + } + elseif ($this->require_acl_invite == 'groups' && $GLOBALS['egw']->accounts->get_type($uid) != 'g') + { + $ret = true; // grant only required for groups + } + else + { + $ret = $this->check_perms(EGW_ACL_INVITE,0,$uid); + } + //error_log(__METHOD__."($uid) = ".array2string($ret)); + //echo "

".__METHOD__."($uid) require_acl_invite=$this->require_acl_invite returning ".array2string($ret)."

\n"; + return $ret; + } + + /** + * Check for added, modified or deleted participants AND notify them + * + * @param array $new_event the updated event + * @param array $old_event the event before the update + * @todo check if there is a real change, not assume every save is a change + */ + function check4update($new_event,$old_event) + { + //error_log(__METHOD__."($new_event[title])"); + $modified = $added = $deleted = array(); + + //echo "

calendar_boupdate::check4update() new participants = ".print_r($new_event['participants'],true).", old participants =".print_r($old_event['participants'],true)."

\n"; + + // Find modified and deleted participants ... + foreach($old_event['participants'] as $old_userid => $old_status) + { + if(isset($new_event['participants'][$old_userid])) + { + $modified[$old_userid] = $new_event['participants'][$old_userid]; + } + else + { + $deleted[$old_userid] = $old_status; + } + } + // Find new participants ... + foreach((array)$new_event['participants'] as $new_userid => $new_status) + { + if(!isset($old_event['participants'][$new_userid])) + { + $added[$new_userid] = 'U'; + } + } + //echo "

calendar_boupdate::check4update() added=".print_r($added,true).", modified=".print_r($modified,true).", deleted=".print_r($deleted,true)."

\n"; + if(count($added) || count($modified) || count($deleted)) + { + if(count($added)) + { + $this->send_update(MSG_ADDED,$added,$old_event,$new_event); + } + if(count($modified)) + { + $this->send_update(MSG_MODIFIED,$modified,$old_event,$new_event); + } + if(count($deleted)) + { + $this->send_update(MSG_DISINVITE,$deleted,$new_event); + } + } + } + + /** + * checks if $userid has requested (in $part_prefs) updates for $msg_type + * + * @param int $userid numerical user-id + * @param array $part_prefs preferces of the user $userid + * @param int $msg_type type of the notification: MSG_ADDED, MSG_MODIFIED, MSG_ACCEPTED, ... + * @param array $old_event Event before the change + * @param array $new_event Event after the change + * @return boolean true = update requested, flase otherwise + */ + function update_requested($userid,$part_prefs,$msg_type,$old_event,$new_event) + { + if ($msg_type == MSG_ALARM) + { + return True; // always True for now + } + $want_update = 0; + + // the following switch falls through all cases, as each included the following too + // + $msg_is_response = $msg_type == MSG_REJECTED || $msg_type == MSG_ACCEPTED || $msg_type == MSG_TENTATIVE || $msg_type == MSG_DELEGATED; + + switch($ru = $part_prefs['calendar']['receive_updates']) + { + case 'responses': + ++$want_update; + case 'modifications': + if (!$msg_is_response) + { + ++$want_update; + } + case 'time_change_4h': + case 'time_change': + $diff = max(abs($this->date2ts($old_event['start'])-$this->date2ts($new_event['start'])), + abs($this->date2ts($old_event['end'])-$this->date2ts($new_event['end']))); + $check = $ru == 'time_change_4h' ? 4 * 60 * 60 - 1 : 0; + if ($msg_type == MSG_MODIFIED && $diff > $check) + { + ++$want_update; + } + case 'add_cancel': + if ($old_event['owner'] == $userid && $msg_is_response || + $msg_type == MSG_DELETED || $msg_type == MSG_ADDED || $msg_type == MSG_DISINVITE) + { + ++$want_update; + } + break; + case 'no': + break; + } + //error_log(__METHOD__."(userid=$userid,,msg_type=$msg_type,...) msg_is_response=$msg_is_response, want_update=$want_update"); + return $want_update > 0; + } + + /** + * sends update-messages to certain participants of an event + * + * @param int $msg_type type of the notification: MSG_ADDED, MSG_MODIFIED, MSG_ACCEPTED, ... + * @param array $to_notify numerical user-ids as keys (!) (value is not used) + * @param array $old_event Event before the change + * @param array $new_event=null Event after the change + * @param int $user=0 User who started the notify, default current user + * @return bool true/false + */ + function send_update($msg_type,$to_notify,$old_event,$new_event=null,$user=0) + { + //error_log(__METHOD__."($msg_type,".array2string($to_notify).",...)"); + if (!is_array($to_notify)) + { + $to_notify = array(); + } + $disinvited = $msg_type == MSG_DISINVITE ? array_keys($to_notify) : array(); + + $owner = $old_event ? $old_event['owner'] : $new_event['owner']; + if ($owner && !isset($to_notify[$owner]) && $msg_type != MSG_ALARM) + { + $to_notify[$owner] = 'owner'; // always include the event-owner + } + $version = $GLOBALS['egw_info']['apps']['calendar']['version']; + + // ignore events in the past (give a tolerance of 10 seconds for the script) + if($old_event != False && $this->date2ts($old_event['start']) < ($this->now_su - 10)) + { + return False; + } + $temp_user = $GLOBALS['egw_info']['user']; // save user-date of the enviroment to restore it after + + if (!$user) + { + $user = $temp_user['account_id']; + } + if ($GLOBALS['egw']->preferences->account_id != $user) + { + $GLOBALS['egw']->preferences->__construct($user); + $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(); + } + $senderid = $GLOBALS['egw_info']['user']['account_id']; + $event = $msg_type == MSG_ADDED || $msg_type == MSG_MODIFIED ? $new_event : $old_event; + + switch($msg_type) + { + case MSG_DELETED: + $action = lang('Canceled'); + $msg = 'Canceled'; + $msgtype = '"calendar";'; + $method = 'CANCEL'; + break; + case MSG_MODIFIED: + $action = lang('Modified'); + $msg = 'Modified'; + $msgtype = '"calendar"; Version="'.$version.'"; Id="'.$new_event['id'].'"'; + $method = 'REQUEST'; + break; + case MSG_DISINVITE: + $action = lang('Disinvited'); + $msg = 'Disinvited'; + $msgtype = '"calendar";'; + $method = 'CANCEL'; + break; + case MSG_ADDED: + $action = lang('Added'); + $msg = 'Added'; + $msgtype = '"calendar"; Version="'.$version.'"; Id="'.$new_event['id'].'"'; + $method = 'REQUEST'; + break; + case MSG_REJECTED: + $action = lang('Rejected'); + $msg = 'Response'; + $msgtype = '"calendar";'; + $method = 'REPLY'; + break; + case MSG_TENTATIVE: + $action = lang('Tentative'); + $msg = 'Response'; + $msgtype = '"calendar";'; + $method = 'REPLY'; + break; + case MSG_ACCEPTED: + $action = lang('Accepted'); + $msg = 'Response'; + $msgtype = '"calendar";'; + $method = 'REPLY'; + break; + case MSG_DELEGATED: + $action = lang('Delegated'); + $msg = 'Response'; + $msgtype = '"calendar";'; + $method = 'REPLY'; + break; + case MSG_ALARM: + $action = lang('Alarm'); + $msg = 'Alarm'; + $msgtype = '"calendar";'; + $method = 'PUBLISH'; // duno if thats right + break; + default: + $method = 'PUBLISH'; + } + $notify_msg = $this->cal_prefs['notify'.$msg]; + if (empty($notify_msg)) + { + $notify_msg = $this->cal_prefs['notifyAdded']; // use a default + } + $details = $this->_get_event_details($event,$action,$event_arr,$disinvited); + + // add all group-members to the notification, unless they are already participants + foreach($to_notify as $userid => $statusid) + { + if (is_numeric($userid) && $GLOBALS['egw']->accounts->get_type($userid) == 'g' && + ($members = $GLOBALS['egw']->accounts->member($userid))) + { + foreach($members as $member) + { + $member = $member['account_id']; + if (!isset($to_notify[$member])) + { + $to_notify[$member] = 'G'; // Group-invitation + } + } + } + } + $user_prefs = $GLOBALS['egw_info']['user']['preferences']; + foreach($to_notify as $userid => $statusid) + { + if ($this->debug > 0) error_log(__METHOD__." trying to notify $userid, with $statusid"); + if (!is_numeric($userid)) + { + $res_info = $this->resource_info($userid); + $userid = $res_info['responsible']; + if (!isset($userid)) continue; + } + + if ($statusid == 'R' || $GLOBALS['egw']->accounts->get_type($userid) == 'g') + { + continue; // dont notify rejected participants or groups + } + + if($userid != $GLOBALS['egw_info']['user']['account_id'] || + ($userid == $GLOBALS['egw_info']['user']['account_id'] && + $user_prefs['calendar']['receive_own_updates']==1) || + $msg_type == MSG_ALARM) + { + $preferences = CreateObject('phpgwapi.preferences',$userid); + $part_prefs = $preferences->read_repository(); + + if (!$this->update_requested($userid,$part_prefs,$msg_type,$old_event,$new_event)) + { + continue; + } + $GLOBALS['egw']->accounts->get_account_name($userid,$lid,$details['to-firstname'],$details['to-lastname']); + $details['to-fullname'] = $GLOBALS['egw']->common->display_fullname('',$details['to-firstname'],$details['to-lastname']); + + $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset'] = $part_prefs['common']['tz_offset']; + $GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] = $part_prefs['common']['timeformat']; + $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'] = $part_prefs['common']['dateformat']; + + $GLOBALS['egw']->datetime->tz_offset = 3600 * (int) $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; + + // event is in user-time of current user, now we need to calculate the tz-difference to the notified user and take it into account + $tz_diff = $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset'] - $this->common_prefs['tz_offset']; + if($old_event != False) $details['olddate'] = $this->format_date($old_event['start']+$tz_diff); + $details['startdate'] = $this->format_date($event['start']+$tz_diff); + $details['enddate'] = $this->format_date($event['end']+$tz_diff); + + list($subject,$body) = explode("\n",$GLOBALS['egw']->preferences->parse_notify($notify_msg,$details),2); + + switch($part_prefs['calendar']['update_format']) + { + case 'ical': + if ($method == 'REQUEST') + { + $ics = ExecMethod2('calendar.calendar_ical.exportVCal',$event['id'],'2.0',$method); + $attachment = array( 'string' => $ics, + 'filename' => 'cal.ics', + 'encoding' => '8bit', + 'type' => 'text/calendar; method='.$method, + ); + } + // fall through + case 'extended': + $body .= "\n\n".lang('Event Details follow').":\n"; + foreach($event_arr as $key => $val) + { + if(strlen($details[$key])) { + switch($key){ + case 'access': + case 'priority': + case 'link': + break; + default: + $body .= sprintf("%-20s %s\n",$val['field'].':',$details[$key]); + break; + } + } + } + break; + } + // send via notification_app + if($GLOBALS['egw_info']['apps']['notifications']['enabled']) { + try { + $notification = new notifications(); + $notification->set_receivers(array($userid)); + $notification->set_message($body); + $notification->set_sender($senderid); + $notification->set_subject($subject); + $notification->set_links(array($details['link_arr'])); + if(is_array($attachment)) { $notification->set_attachments(array($attachment)); } + $notification->send(); + } + catch (Exception $exception) { + error_log(__METHOD__.' error while notifying user '.$userid.':'.$exception->getMessage()); + continue; + } + } else { + error_log(__METHOD__.' cannot send any notifications because notifications is not installed'); + } + } + } + // restore the enviroment (preferences->read_repository() sets the timezone!) + $GLOBALS['egw_info']['user'] = $temp_user; + if ($GLOBALS['egw']->preferences->account_id != $temp_user['account_id'] || isset($preferences)) + { + $GLOBALS['egw']->preferences->__construct($temp_user['account_id']); + $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(); + //echo "

".__METHOD__."() restored enviroment of #$temp_user[account_id] $temp_user[account_fullname]: tz={$GLOBALS['egw_info']['user']['preferences']['common']['tz']}

\n"; + } + return true; + } + + function get_update_message($event,$added) + { + $details = $this->_get_event_details($event,$added ? lang('Added') : lang('Modified'),$nul); + + $notify_msg = $this->cal_prefs[$added || empty($this->cal_prefs['notifyModified']) ? 'notifyAdded' : 'notifyModified']; + + return explode("\n",$GLOBALS['egw']->preferences->parse_notify($notify_msg,$details),2); + } + + /** + * Function called via async service, when an alarm is to be send + * + * @param array $alarm array with keys owner, cal_id, all + * @return boolean + */ + function send_alarm($alarm) + { + //echo "

bocalendar::send_alarm("; print_r($alarm); echo ")

\n"; + $GLOBALS['egw_info']['user']['account_id'] = $this->owner = $alarm['owner']; + + $event_time_user = egw_time::server2user($alarm['time'] + $alarm['offset']); // alarm[time] is in server-time, read requires user-time + if (!$alarm['owner'] || !$alarm['cal_id'] || !($event = $this->read($alarm['cal_id'],$event_time_user))) + { + return False; // event not found + } + if ($alarm['all']) + { + $to_notify = $event['participants']; + } + elseif ($this->check_perms(EGW_ACL_READ,$event)) // checks agains $this->owner set to $alarm[owner] + { + $to_notify[$alarm['owner']] = 'A'; + } + else + { + return False; // no rights + } + $ret = $this->send_update(MSG_ALARM,$to_notify,$event,False,$alarm['owner']); + + // create a new alarm for recuring events for the next event, if one exists + if ($event['recur_type'] && ($event = $this->read($alarm['cal_id'],$event_time_user+1))) + { + $alarm['time'] = $this->date2ts($event['start']) - $alarm['offset']; + + $this->save_alarm($alarm['cal_id'],$alarm); + } + return $ret; + } + + /** + * saves an event to the database, does NOT do any notifications, see calendar_boupdate::update for that + * + * 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 + * @param boolean $updateTS=true update the content history of the event + * @return int|boolean $cal_id > 0 or false on error (eg. permission denied) + */ + function save($event,$ignore_acl=false,$updateTS=true) + { + //echo '

'.__METHOD__.'('.array2string($event).",$ignore_acl)

\n"; + //error_log(__METHOD__.'('.array2string($event).",$etag)"); + // check if user has the permission to update / create the event + if (!$ignore_acl && ($event['id'] && !$this->check_perms(EGW_ACL_EDIT,$event['id']) || + !$event['id'] && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner']) && + !$this->check_perms(EGW_ACL_ADD,0,$event['owner']))) + { + return false; + } + + // invalidate the read-cache if it contains the event we store now + if ($event['id'] && $event['id'] == self::$cached_event['id']) self::$cached_event = array(); + + $save_event = $event; + // we run all dates through date2ts, to adjust to server-time and the possible date-formats + foreach(array('start','end','modified','created','recur_enddate','recurrence') as $ts) + { + // we convert here from user-time to timestamps in server-time! + if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2ts($event[$ts],true) : 0; + } + // convert tzid name to integer tz_id, of set user default + if (empty($event['tzid']) || !($event['tz_id'] = calendar_timezones::tz2id($event['tzid']))) + { + $event['tz_id'] = calendar_timezones::tz2id($event['tzid'] = egw_time::$user_timezone->getName()); + } + // same with the recur exceptions + if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + { + foreach($event['recur_exception'] as $n => $date) + { + $event['recur_exception'][$n] = $this->date2ts($date,true); + } + } + // same with the alarms + if (isset($event['alarm']) && is_array($event['alarm'])) + { + foreach($event['alarm'] as $id => $alarm) + { + $event['alarm'][$id]['time'] = $this->date2ts($alarm['time'],true); + } + } + if (!isset($event['modified']) || $event['modified'] > $this->now) + { + $event['modified'] = $this->now; + $event['modifier'] = $this->user; + } + if (empty($event['id']) && (!isset($event['created']) || $event['created'] > $this->now)) + { + $event['created'] = $this->now; + $event['creator'] = $this->user; + } + $set_recurrences = false; + $set_recurrences_start = 0; + if (($cal_id = $this->so->save($event,$set_recurrences,$set_recurrences_start,0,$event['etag'])) && $set_recurrences && $event['recur_type'] != MCAL_RECUR_NONE) + { + $save_event['id'] = $cal_id; + // unset participants to enforce the default stati for all added recurrences + unset($save_event['participants']); + $this->set_recurrences($save_event, $set_recurrences_start); + } + if ($updateTS) $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id,$event['id'] ? 'modify' : 'add',time()); + + return $cal_id; + } + + /** + * Check if the current user has the necessary ACL rights to change the status of $uid + * + * For contacts we use edit rights of the owner of the event (aka. edit rights of the event). + * + * @param int|string $uid account_id or 1-char type-identifer plus id (eg. c15 for addressbook entry #15) + * @param array|int $event event array or id of the event + * @return boolean + */ + function check_status_perms($uid,$event) + { + if ($uid[0] == 'c' || $uid[0] == 'e') // for contact we use the owner of the event + { + if (!is_array($event) && !($event = $this->read($event))) return false; + + return $this->check_perms(EGW_ACL_EDIT,0,$event['owner']); + } + // check if we have a category acl for the event or not (null) + $access = $this->check_cat_acl(self::CAT_ACL_STATUS,$event); + if (!is_null($access)) + { + return $access; + } + // no access or denied access because of category acl --> regular check + if (!is_numeric($uid)) // this is eg. for resources (r123) + { + $resource = $this->resource_info($uid); + + return EGW_ACL_EDIT & $resource['rights']; + } + // regular user and groups + return $this->check_perms(EGW_ACL_EDIT,0,$uid); + } + + /** + * Check if current user has a certain right on the categories of an event + * + * Not having the given right for a single category, means not having it! + * + * @param int $right self::CAT_ACL_{ADD|STATUS} + * @param int|array $event + * @return boolean true if use has the right, false if not + * @return boolean false=access denied because of cat acl, true access granted because of cat acl, + * null = cat has no acl + */ + function check_cat_acl($right,$event) + { + if (!is_array($event)) $event = $this->read($event); + + $ret = null; + if ($event['category']) + { + foreach(is_array($event['category']) ? $event['category'] : explode(',',$event['category']) as $cat_id) + { + $access = self::has_cat_right($right,$cat_id,$this->user); + if ($access === true) + { + $ret = true; + break; + } + if ($access === false) + { + $ret = false; // cat denies access --> check further cats + } + } + } + //echo "

".__METHOD__."($event[id]: $event[title], $right) = ".array2string($ret)."

\n"; + return $ret; + } + + /** + * Array with $cat_id => $rights pairs for current user (no entry means, cat is not limited by ACL!) + * + * @var array + */ + private static $cat_rights_cache; + + /** + * Get rights for a given category id + * + * @param int $cat_id=null null to return array with all cats + * @return array with account_id => right pairs + */ + public static function get_cat_rights($cat_id=null) + { + if (!isset(self::$cat_rights_cache)) + { + self::$cat_rights_cache = egw_cache::getSession('calendar','cat_rights', + array($GLOBALS['egw']->acl,'get_location_grants'),array('L%','calendar')); + } + //echo "

".__METHOD__."($cat_id) = ".array2string($cat_id ? self::$cat_rights_cache['L'.$cat_id] : self::$cat_rights_cache)."

\n"; + return $cat_id ? self::$cat_rights_cache['L'.$cat_id] : self::$cat_rights_cache; + } + + /** + * Set rights for a given single category and user + * + * @param int $cat_id + * @param int $user + * @param int $rights self::CAT_ACL_{ADD|STATUS} or'ed together + */ + public static function set_cat_rights($cat_id,$user,$rights) + { + //echo "

".__METHOD__."($cat_id,$user,$rights)

\n"; + if (!isset(self::$cat_rights_cache)) self::get_cat_rights($cat_id); + + if ((int)$rights != (int)self::$cat_rights_cache['L'.$cat_id][$user]) + { + if ($rights) + { + self::$cat_rights_cache['L'.$cat_id][$user] = $rights; + $GLOBALS['egw']->acl->add_repository('calendar','L'.$cat_id,$user,$rights); + } + else + { + unset(self::$cat_rights_cache['L'.$cat_id][$user]); + if (!self::$cat_rights_cache['L'.$cat_id]) unset(self::$cat_rights_cache['L'.$cat_id]); + $GLOBALS['egw']->acl->delete_repository('calendar','L'.$cat_id,$user); + } + egw_cache::setSession('calendar','cat_rights',self::$cat_rights_cache); + } + } + + /** + * Check if current user has a given right on a category (if it's restricted!) + * + * @param int $cat_id + * @return boolean false=access denied because of cat acl, true access granted because of cat acl, + * null = cat has no acl + */ + public static function has_cat_right($right,$cat_id,$user) + { + static $cache; + + if (!isset($cache[$cat_id])) + { + $all = $own = 0; + $cat_rights = self::get_cat_rights($cat_id); + if (!is_null($cat_rights)) + { + static $memberships; + if (is_null($memberships)) + { + $memberships = $GLOBALS['egw']->accounts->memberships($user,true); + $memberships[] = $user; + } + foreach($cat_rights as $uid => $value) + { + $all |= $value; + if (in_array($uid,$memberships)) $own |= $value; + } + } + foreach(array(self::CAT_ACL_ADD,self::CAT_ACL_STATUS) as $mask) + { + $cache[$cat_id][$mask] = !($all & $mask) ? null : !!($own & $mask); + } + } + //echo "

".__METHOD__."($right,$cat_id) all=$all, own=$own returning ".array2string($cache[$cat_id][$right])."

\n"; + return $cache[$cat_id][$right]; + } + + /** + * set the status of one participant for a given recurrence or for all recurrences since now (includes recur_date=0) + * + * @param int|array $event event-array or id of the event + * @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 + * @param boolean $updateTS=true update the content history of the event + * @return int number of changed recurrences + */ + function set_status($event,$uid,$status,$recur_date=0,$ignore_acl=false,$updateTS=true) + { + $cal_id = is_array($event) ? $event['id'] : $event; + //echo "

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

\n"; + if (!$cal_id || (!$ignore_acl && !$this->check_status_perms($uid,$event))) + { + return false; + } + calendar_so::split_status($status, $quantity, $role); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($cal_id, $uid, $status, $recur_date)\n",3,$this->logfile); + } + if (($Ok = $this->so->set_status($cal_id,is_numeric($uid)?'u':$uid[0], + is_numeric($uid)?$uid:substr($uid,1),$status, + $recur_date?$this->date2ts($recur_date,true):0,$role))) + { + if ($updateTS) $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id,'modify',time()); + + static $status2msg = array( + 'R' => MSG_REJECTED, + 'T' => MSG_TENTATIVE, + 'A' => MSG_ACCEPTED, + 'D' => MSG_DELEGATED, + ); + if (isset($status2msg[$status])) + { + if (!is_array($event)) $event = $this->read($cal_id); + if (isset($recur_date)) $event = $this->read($event['id'],$recur_date); //re-read the actually edited recurring event + $this->send_update($status2msg[$status],$event['participants'],$event); + } + + } + return $Ok; + } + + /** + * 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=0) + { + if (!isset($new_event['participants'])) return; + + // check the old list against the new list + foreach ($old_event['participants'] as $userid => $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]); + } + } + // write the changes + foreach ($new_event['participants'] as $userid => $status) + { + $this->set_status($old_event, $userid, $status, $recur_date, true, false); + } + } + + /** + * deletes an event + * + * @param int $cal_id id of the event to delete + * @param int $recur_date=0 if a single event from a series should be deleted, its date + * @param boolean $ignore_acl=false true for no ACL check, default do ACL check + * @return boolean true on success, false on error (usually permission denied) + */ + function delete($cal_id,$recur_date=0,$ignore_acl=false) + { + if (!($event = $this->read($cal_id,$recur_date)) || + !$ignore_acl && !$this->check_perms(EGW_ACL_DELETE,$event)) + { + return false; + } + $this->send_update(MSG_DELETED,$event['participants'],$event); + + if (!$recur_date || $event['recur_type'] == MCAL_RECUR_NONE) + { + $this->so->delete($cal_id); + $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id,'delete',time()); + + // delete all links to the event + egw_link::unlink(0,'calendar',$cal_id); + } + else + { + $event['recur_exception'][] = $recur_date = $this->date2ts($event['start']); + unset($event['start']); + unset($event['end']); + $this->save($event); // updates the content-history + } + if ($event['reference']) + { + // evtl. delete recur_exception $event['recurrence'] from event with cal_id=$event['reference'] + } + return true; + } + + /** + * helper for send_update and get_update_message + * @internal + */ + function _get_event_details($event,$action,&$event_arr,$disinvited=array()) + { + $details = array( // event-details for the notify-msg + 'id' => $event['id'], + 'action' => $action, + ); + $event_arr = $this->event2array($event); + foreach($event_arr as $key => $val) + { + $details[$key] = $val['data']; + } + $details['participants'] = $details['participants'] ? implode("\n",$details['participants']) : ''; + + $event_arr['link']['field'] = lang('URL'); + $eventStart_arr = $this->date2array($event['start']); // give this as 'date' to the link to pick the right recurrence for the participants state + $link = $GLOBALS['egw_info']['server']['webserver_url'].'/index.php?menuaction=calendar.calendar_uiforms.edit&cal_id='.$event['id'].'&date='.$eventStart_arr['full'].'&no_popup=1'; + // if url is only a path, try guessing the rest ;-) + if ($link[0] == '/') + { + $link = ($GLOBALS['egw_info']['server']['enforce_ssl'] || $_SERVER['HTTPS'] ? 'https://' : 'http://'). + ($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']). + $link; + } + $event_arr['link']['data'] = $details['link'] = $link; + + /* this is needed for notification-app + * notification-app creates the link individual for + * every user, so we must provide a neutral link-style + * if calendar implements tracking in near future, this part can be deleted + */ + $link_arr = array(); + $link_arr['text'] = $event['title']; + $link_arr['view'] = array( 'menuaction' => 'calendar.calendar_uiforms.edit', + 'cal_id' => $event['id'], + 'date' => $eventStart_arr['full'], + ); + $link_arr['popup'] = '750x400'; + $details['link_arr'] = $link_arr; + + $dis = array(); + foreach($disinvited as $uid) + { + $dis[] = $this->participant_name($uid); + } + $details['disinvited'] = implode(', ',$dis); + return $details; + } + + /** + * create array with name, translated name and readable content of each attributes of an event + * + * old function, so far only used by send_update (therefor it's in bocalupdate and not bocal) + * + * @param array $event event to use + * @returns array of attributes with fieldname as key and array with the 'field'=translated name 'data' = readable content (for participants this is an array !) + */ + function event2array($event) + { + $var['title'] = Array( + 'field' => lang('Title'), + 'data' => $event['title'] + ); + + $var['description'] = Array( + 'field' => lang('Description'), + 'data' => $event['description'] + ); + + foreach(explode(',',$event['category']) as $cat_id) + { + list($cat) = $GLOBALS['egw']->categories->return_single($cat_id); + $cat_string[] = stripslashes($cat['name']); + } + $var['category'] = Array( + 'field' => lang('Category'), + 'data' => implode(', ',$cat_string) + ); + + $var['location'] = Array( + 'field' => lang('Location'), + 'data' => $event['location'] + ); + + $var['startdate'] = Array( + 'field' => lang('Start Date/Time'), + 'data' => $this->format_date($event['start']), + ); + + $var['enddate'] = Array( + 'field' => lang('End Date/Time'), + 'data' => $this->format_date($event['end']), + ); + + $pri = Array( + 0 => '', + 1 => lang('Low'), + 2 => lang('Normal'), + 3 => lang('High') + ); + $var['priority'] = Array( + 'field' => lang('Priority'), + 'data' => $pri[$event['priority']] + ); + + $var['owner'] = Array( + 'field' => lang('Owner'), + 'data' => $GLOBALS['egw']->common->grab_owner_name($event['owner']) + ); + + $var['updated'] = Array( + 'field' => lang('Updated'), + 'data' => $this->format_date($event['modtime']).', '.$GLOBALS['egw']->common->grab_owner_name($event['modifier']) + ); + + $var['access'] = Array( + 'field' => lang('Access'), + 'data' => $event['public'] ? lang('Public') : lang('Private') + ); + + if (isset($event['participants']) && is_array($event['participants'])) + { + $participants = $this->participants($event,true); + } + $var['participants'] = Array( + 'field' => lang('Participants'), + 'data' => $participants + ); + + // Repeated Events + if($event['recur_type'] != MCAL_RECUR_NONE) + { + $var['recur_type'] = Array( + 'field' => lang('Repetition'), + 'data' => $this->recure2string($event), + ); + } + return $var; + } + + /** + * log all updates to a file + * + * @param array $event2save event-data before calling save + * @param array $event_saved event-data read back from the DB + * @param array $old_event=null event-data in the DB before calling save + * @param string $type='update' + */ + function log2file($event2save,$event_saved,$old_event=null,$type='update') + { + if (!($f = fopen($this->log_file,'a'))) + { + echo "

error opening '$this->log_file' !!!

\n"; + return false; + } + fwrite($f,$type.': '.$GLOBALS['egw']->common->grab_owner_name($this->user).': '.date('r')."\n"); + fwrite($f,"Time: time to save / saved time read back / old time before save\n"); + foreach(array('start','end') as $name) + { + fwrite($f,$name.': '.(isset($event2save[$name]) ? $this->format_date($event2save[$name]) : 'not set').' / '. + $this->format_date($event_saved[$name]) .' / '. + (is_null($old_event) ? 'no old event' : $this->format_date($old_event[$name]))."\n"); + } + foreach(array('event2save','event_saved','old_event') as $name) + { + fwrite($f,$name.' = '.print_r($$name,true)); + } + fwrite($f,"\n"); + fclose($f); + + return true; + } + + /** + * saves a new or updated alarm + * + * @param int $cal_id Id of the calendar-entry + * @param array $alarm array with fields: text, owner, enabled, .. + * @return string id of the alarm, or false on error (eg. no perms) + */ + function save_alarm($cal_id,$alarm) + { + if (!$cal_id || !$this->check_perms(EGW_ACL_EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0)) + { + //echo "

no rights to save the alarm=".print_r($alarm,true)." to event($cal_id)

"; + return false; // no rights to add the alarm + } + $alarm['time'] = $this->date2ts($alarm['time'],true); // user to server-time + + $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id, 'modify', time()); + + return $this->so->save_alarm($cal_id,$alarm, $this->now_su); + } + + /** + * 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 + * @return int number of alarms deleted, false on error (eg. no perms) + */ + function delete_alarm($id) + { + list(,$cal_id) = explode(':',$id); + + if (!($alarm = $this->so->read_alarm($id)) || !$cal_id || !$this->check_perms(EGW_ACL_EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0)) + { + return false; // no rights to delete the alarm + } + + $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id, 'modify', time()); + + return $this->so->delete_alarm($id, $this->now_su); + } + + /** + * Find existing categories in database by name or add categories that do not exist yet + * currently used for ical/sif import + * + * @param array $catname_list names of the categories which should be found or added + * @param int $cal_id=-1 match against existing event and expand the returned category ids + * by the ones the user normally does not see due to category permissions - used to preserve categories + * @return array category ids (found, added and preserved categories) + */ + function find_or_add_categories($catname_list, $cal_id=-1) + { + if ($cal_id && $cal_id > 0) + { + // preserve categories without users read access + $old_event = $this->read($cal_id); + $old_categories = explode(',',$old_event['category']); + $old_cats_preserve = array(); + if (is_array($old_categories) && count($old_categories) > 0) + { + foreach ($old_categories as $cat_id) + { + if (!$this->categories->check_perms(EGW_ACL_READ, $cat_id)) + { + $old_cats_preserve[] = $cat_id; + } + } + } + } + + $cat_id_list = array(); + foreach ((array)$catname_list as $cat_name) + { + $cat_name = trim($cat_name); + $cat_id = $this->categories->name2id($cat_name, 'X-'); + + if (!$cat_id) + { + // some SyncML clients (mostly phones) add an X- to the category names + if (strncmp($cat_name, 'X-', 2) == 0) + { + $cat_name = substr($cat_name, 2); + } + $cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private')); + } + + if ($cat_id) + { + $cat_id_list[] = $cat_id; + } + } + + if (is_array($old_cats_preserve) && count($old_cats_preserve) > 0) + { + $cat_id_list = array_merge($cat_id_list, $old_cats_preserve); + } + + if (count($cat_id_list) > 1) + { + $cat_id_list = array_unique($cat_id_list); + sort($cat_id_list, SORT_NUMERIC); + } + + return $cat_id_list; + } + + function get_categories($cat_id_list) + { + if (!is_array($cat_id_list)) + { + $cat_id_list = explode(',',$cat_id_list); + } + $cat_list = array(); + foreach ($cat_id_list as $cat_id) + { + if ($cat_id && $this->categories->check_perms(EGW_ACL_READ, $cat_id) && + ($cat_name = $this->categories->id2name($cat_id)) && $cat_name != '--') + { + $cat_list[] = $cat_name; + } + } + + return $cat_list; + } + + /** + * Try to find a matching db entry + * + * @param array $event the vCalendar data we try to find + * @param string filter='exact' exact -> find the matching entry + * check -> check (consitency) for identical matches + * relax -> be more tolerant + * master -> try to find a releated series master + * @return array calendar_ids of matching entries + */ + function find_event($event, $filter='exact') + { + $matchingEvents = array(); + $query = array(); + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "($filter)[EVENT]:" . array2string($event)."\n",3,$this->logfile); + } + + if ($filter == 'master') + { + $query[] = 'recur_type!='. MCAL_RECUR_NONE; + $query['cal_recurrence'] = 0; + } + + if (!isset($event['recurrence'])) $event['recurrence'] = 0; + + if ($event['id']) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['id'] . ")[EventID]\n",3,$this->logfile); + } + if (($egwEvent = $this->read($event['id'], 0, false, 'server'))) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '()[FOUND]:' . array2string($egwEvent)."\n",3,$this->logfile); + } + if ($egwEvent['recur_type'] != MCAL_RECUR_NONE && + (empty($event['uid']) || $event['uid'] == $egwEvent['uid'])) + { + if ($filter == 'master') + { + $matchingEvents[] = $egwEvent['id']; // we found the master + } + if ($event['recur_type'] == $egwEvent['recur_type']) + { + $matchingEvents[] = $egwEvent['id']; // we found the event + } + elseif ($event['recur_type'] == MCAL_RECUR_NONE && + $event['recurrence'] != 0) + { + $exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['tzid']); + if (in_array($event['recurrence'], $exceptions)) + { + $matchingEvents[] = $egwEvent['id'] . ':' . (int)$event['recurrence']; + } + } + } elseif ($filter != 'master' && ($filter == 'exact' || + $event['recur_type'] == $egwEvent['recur_type'] && + strpos($egwEvent['title'], $event['title']) === 0)) + { + $matchingEvents[] = $egwEvent['id']; // we found the event + } + } + if (!empty($matchingEvents) || $filter == 'exact') return $matchingEvents; + } + unset($event['id']); + + // No chance to find a master without [U]ID + if ($filter == 'master' && empty($event['uid'])) return $matchingEvents; + + // only query calendars of users, we have READ-grants from + $users = array(); + foreach(array_keys($this->grants) as $user) + { + $user = trim($user); + if ($this->check_perms(EGW_ACL_READ|EGW_ACL_READ_FOR_PARTICIPANTS|EGW_ACL_FREEBUSY,0,$user)) + { + if ($user && !in_array($user,$users)) // already added? + { + $users[] = $user; + } + } + elseif ($GLOBALS['egw']->accounts->get_type($user) != 'g') + { + continue; // for non-groups (eg. users), we stop here if we have no read-rights + } + // the further code is only for real users + if (!is_numeric($user)) continue; + + // for groups we have to include the members + if ($GLOBALS['egw']->accounts->get_type($user) == 'g') + { + $members = $GLOBALS['egw']->accounts->member($user); + if (is_array($members)) + { + foreach($members as $member) + { + // use only members which gave the user a read-grant + if (!in_array($member['account_id'],$users) && + $this->check_perms(EGW_ACL_READ|EGW_ACL_FREEBUSY,0,$member['account_id'])) + { + $users[] = $member['account_id']; + } + } + } + } + else // for users we have to include all the memberships, to get the group-events + { + $memberships = $GLOBALS['egw']->accounts->membership($user); + if (is_array($memberships)) + { + foreach($memberships as $group) + { + if (!in_array($group['account_id'],$users)) + { + $users[] = $group['account_id']; + } + } + } + } + } + + if ($filter != 'master' && ($filter != 'exact' || empty($event['uid']))) + { + if (!empty($event['whole_day'])) + { + if ($filter == 'relax') + { + $delta = 1800; + } + else + { + $delta = 60; + } + + // check length with some tolerance + $length = $event['end'] - $event['start'] - $delta; + $query[] = ('(cal_end-cal_start)>' . $length); + $length += 2 * $delta; + $query[] = ('(cal_end-cal_start)<' . $length); + $query[] = ('cal_start>' . ($event['start'] - 86400)); + $query[] = ('cal_start<' . ($event['start'] + 86400)); + } + elseif (isset($event['start'])) + { + + if ($filter == 'relax') + { + $query[] = ('cal_start>' . ($event['start'] - 3600)); + $query[] = ('cal_start<' . ($event['start'] + 3600)); + } + else + { + // we accept a tiny tolerance + $query[] = ('cal_start>' . ($event['start'] - 2)); + $query[] = ('cal_start<' . ($event['start'] + 2)); + } + } + if ($filter == 'relax') + { + $matchFields = array(); + } + else + { + $matchFields = array('priority', 'public'); + } + $matchFields[] = 'recurrence'; + foreach ($matchFields as $key) + { + if (isset($event[$key])) $query['cal_'.$key] = $event[$key]; + } + } + + if (!empty($event['uid'])) + { + $query['cal_uid'] = $event['uid']; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $event['uid'] . ")[EventUID]\n",3,$this->logfile); + } + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[QUERY]: ' . array2string($query)."\n",3,$this->logfile); + } + if (!count($users) || !($foundEvents = + $this->so->search(null, null, $users, 0, 'owner', $query))) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "[NO MATCH]\n",3,$this->logfile); + } + return $matchingEvents; + } + + $pseudos = array(); + + foreach($foundEvents as $egwEvent) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[FOUND]: ' . array2string($egwEvent)."\n",3,$this->logfile); + } + + if (in_array($egwEvent['id'], $matchingEvents)) continue; + + // convert timezone id of event to tzid (iCal id like 'Europe/Berlin') + if (!$egwEvent['tz_id'] || !($egwEvent['tzid'] = calendar_timezones::id2tz($egwEvent['tz_id']))) + { + $egwEvent['tzid'] = egw_time::$server_timezone->getName(); + } + if (!isset(self::$tz_cache[$egwEvent['tzid']])) + { + self::$tz_cache[$egwEvent['tzid']] = calendar_timezones::DateTimeZone($egwEvent['tzid']); + } + if (!$event['tzid']) + { + $event['tzid'] = egw_time::$server_timezone->getName(); + } + if (!isset(self::$tz_cache[$event['tzid']])) + { + self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']); + } + + if (!empty($event['uid'])) + { + if ($filter == 'master') + { + // We found the master + $matchingEvents = array($egwEvent['id']);; + break; + } + if ($filter == 'exact') + { + // UID found + if (empty($event['recurrence'])) + { + $egwstart = new egw_time($egwEvent['start'], egw_time::$server_timezone); + $egwstart->setTimezone(self::$tz_cache[$egwEvent['tzid']]); + $dtstart = new egw_time($event['start'], egw_time::$server_timezone); + $dtstart->setTimezone(self::$tz_cache[$event['tzid']]); + if ($egwEvent['recur_type'] == MCAL_RECUR_NONE && + $event['recur_type'] == MCAL_RECUR_NONE || + $egwEvent['recur_type'] != MCAL_RECUR_NONE && + $event['recur_type'] != MCAL_RECUR_NONE) + { + if ($egwEvent['recur_type'] == MCAL_RECUR_NONE && + $egwstart->format('Ymd') == $dtstart->format('Ymd') || + $egwEvent['recur_type'] != MCAL_RECUR_NONE) + { + // We found an exact match + $matchingEvents = array($egwEvent['id']); + break; + } + else + { + $matchingEvents[] = $egwEvent['id']; + } + } + continue; + } + elseif ($egwEvent['recurrence'] == $event['recurrence']) + { + // We found an exact match + $matchingEvents = array($egwEvent['id']); + break; + } + if ($egwEvent['recur_type'] != MCAL_RECUR_NONE && + $event['recur_type'] == MCAL_RECUR_NONE && + !$egwEvent['recurrence'] && $event['recurrence']) + { + $exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['tzid']); + if (in_array($event['recurrence'], $exceptions)) + { + // We found a pseudo exception + $matchingEvents = array($egwEvent['id'] . ':' . (int)$event['recurrence']); + break; + } + } + continue; + } + } + + // check times + if ($filter != 'relax') + { + if (empty($event['whole_day'])) + { + if (abs($event['end'] - $egwEvent['end']) >= 120) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() egwEvent length does not match!\n",3,$this->logfile); + } + continue; + } + } + else + { + if (!$this->so->isWholeDay($egwEvent)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() egwEvent is not a whole-day event!\n",3,$this->logfile); + } + continue; + } + } + } + + // check for real match + $matchFields = array('title', 'description'); + if ($filter != 'relax') + { + $matchFields[] = 'location'; + } + foreach ($matchFields as $key) + { + if (!empty($event[$key]) && (empty($egwEvent[$key]) + || strpos(str_replace("\r\n", "\n", $egwEvent[$key]), $event[$key]) !== 0)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() event[$key] differ: '" . $event[$key] . + "' <> '" . $egwEvent[$key] . "'\n",3,$this->logfile); + } + continue 2; // next foundEvent + } + } + + if (is_array($event['category'])) + { + // check categories + $egwCategories = explode(',', $egwEvent['category']); + foreach ($egwCategories as $cat_id) + { + if ($this->categories->check_perms(EGW_ACL_READ, $cat_id) && + !in_array($cat_id, $event['category'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() egwEvent category $cat_id is missing!\n",3,$this->logfile); + } + continue 2; + } + } + $newCategories = array_diff($event['category'], $egwCategories); + if (!empty($newCategories)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() event has additional categories:' + . array2string($newCategories)."\n",3,$this->logfile); + } + continue; + } + } + + if ($filter != 'relax') + { + // check participants + if (is_array($event['participants'])) + { + foreach ($event['participants'] as $attendee => $status) + { + if (!isset($egwEvent['participants'][$attendee]) && + $attendee != $egwEvent['owner']) // || + //(!$relax && $egw_event['participants'][$attendee] != $status)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() additional event['participants']: $attendee\n",3,$this->logfile); + } + continue 2; + } + else + { + unset($egwEvent['participants'][$attendee]); + } + } + // ORGANIZER is maybe missing + unset($egwEvent['participants'][$egwEvent['owner']]); + if (!empty($egwEvent['participants'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() missing event[participants]: ' . + array2string($egwEvent['participants'])."\n",3,$this->logfile); + } + continue; + } + } + } + + if ($event['recur_type'] == MCAL_RECUR_NONE) + { + if ($egwEvent['recur_type'] != MCAL_RECUR_NONE) + { + // We found a pseudo Exception + $pseudos[] = $egwEvent['id'] . ':' . $event['start']; + continue; + } + } + elseif ($filter != 'relax') + { + // check exceptions + // $exceptions[$remote_ts] = $egw_ts + $exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['$tzid'], 0, 0, 'map'); + if (is_array($event['recur_exception'])) + { + foreach ($event['recur_exception'] as $key => $day) + { + if (isset($exceptions[$day])) + { + unset($exceptions[$day]); + } + else + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() additional event['recur_exception']: $day\n",3,$this->logfile); + } + continue 2; + } + } + if (!empty($exceptions)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() missing event[recur_exception]: ' . + array2string($event['recur_exception'])); + } + continue; + } + } + + // check recurrence information + foreach (array('recur_type', 'recur_interval', 'recur_enddate') as $key) + { + if (isset($event[$key]) + && $event[$key] != $egwEvent[$key]) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() events[$key] differ: " . $event[$key] . + ' <> ' . $egwEvent[$key]."\n",3,$this->logfile); + } + continue 2; + } + } + } + $matchingEvents[] = $egwEvent['id']; // exact match + } + if (!empty($event['uid']) && + count($matchingEvents) > 1 || $filter != 'master' && + $egwEvent['recur_type'] != MCAL_RECUR_NONE && + empty($event['recur_type'])) + { + + // Unknown exception for existing series + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() new exception for series found.\n",3,$this->logfile); + } + $matchingEvents = array(); + } + + // append pseudos as last entries + $matchingEvents = array_merge($matchingEvents, $pseudos); + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '[MATCHES]:' . array2string($matchingEvents)."\n",3,$this->logfile); + } + return $matchingEvents; + } + + /** + * classifies an incoming event from the eGW point-of-view + * + * exceptions: unlike other calendar apps eGW does not create an event exception + * if just the participant state changes - therefore we have to distinguish between + * real exceptions and status only exceptions + * + * @param array $event the event to check + * + * @return array + * type => + * SINGLE a single event + * SERIES-MASTER the series master + * SERIES-EXCEPTION event is a real exception + * SERIES-PSEUDO-EXCEPTION event is a status only exception + * SERIES-EXCEPTION-PROPAGATE event was a status only exception in the past and is now a real exception + * stored_event => if event already exists in the database array with event data or false + * master_event => for event type SERIES-EXCEPTION, SERIES-PSEUDO-EXCEPTION or SERIES-EXCEPTION-PROPAGATE + * the corresponding series master event array + * NOTE: this param is false if event is of type SERIES-MASTER + */ + function get_event_info($event) + { + $type = 'SINGLE'; // default + $master_event = false; //default + $stored_event = false; + $recurrence_event = false; + $wasPseudo = false; + + if (($foundEvents = $this->find_event($event, 'exact'))) + { + // We found the exact match + $eventID = array_shift($foundEvents); + if (strstr($eventID, ':')) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; + $wasPseudo = true; + list($eventID, $recur_date) = explode(':', $eventID); + $recur_date = $this->date2usertime($recur_date); + $stored_event = $this->read($eventID, $recur_date, false, 'server'); + $master_event = $this->read($eventID, 0, false, 'server'); + $recurrence_event = $stored_event; + } + else + { + $stored_event = $this->read($eventID, 0, false, 'server'); + } + if (!empty($stored_event['uid']) && empty($event['uid'])) + { + $event['uid'] = $stored_event['uid']; // restore the UID if it was not delivered + } + } + + if ($event['recur_type'] != MCAL_RECUR_NONE) + { + $type = 'SERIES-MASTER'; + } + + if ($type == 'SINGLE' && + ($foundEvents = $this->find_event($event, 'master'))) + { + // SINGLE, SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS + foreach ($foundEvents as $eventID) + { + // Let's try to find a related series + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "()[MASTER]: $eventID\n",3,$this->logfile); + } + $type = 'SERIES-EXCEPTION'; + if (($master_event = $this->read($eventID, 0, false, 'server'))) + { + if (isset($stored_event['id']) && + $master_event['id'] != $stored_event['id']) + { + break; // this is an existing exception + } + elseif (isset($event['recurrence']) && + in_array($event['recurrence'], $master_event['recur_exception'])) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; // could also be a real one + $recurrence_event = $master_event; + $recurrence_event['start'] = $event['recurrence']; + $recurrence_event['end'] -= $master_event['start'] - $event['recurrence']; + break; + } + elseif (in_array($event['start'], $master_event['recur_exception'])) + { + $type='SERIES-PSEUDO-EXCEPTION'; // new pseudo exception? + $recurrence_event = $master_event; + $recurrence_event['start'] = $event['start']; + $recurrence_event['end'] -= $master_event['start'] - $event['start']; + break; + } + else + { + // try to find a suitable pseudo exception date + $egw_rrule = calendar_rrule::event2rrule($master_event, false); + $egw_rrule->current = clone $egw_rrule->time; + while ($egw_rrule->valid()) + { + $occurrence = egw_time::to($egw_rrule->current(), 'server'); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '() try occurrence ' . $egw_rrule->current() + . " ($occurrence)\n",3,$this->logfile); + } + if ($event['start'] == $occurrence) + { + $type = 'SERIES-PSEUDO-EXCEPTION'; // let's try a pseudo exception + $recurrence_event = $master_event; + $recurrence_event['start'] = $occurrence; + $recurrence_event['end'] -= $master_event['start'] - $occurrence; + break 2; + } + if (isset($event['recurrence']) && $event['recurrence'] == $occurrence) + { + $type = 'SERIES-EXCEPTION-PROPAGATE'; + if ($stored_event) + { + unset($stored_event['id']); // signal the true exception + $stored_event['recur_type'] = MCAL_RECUR_NONE; + } + break 2; + } + $egw_rrule->next_no_exception(); + } + } + } + } + } + + // check pseudo exception propagation + if ($recurrence_event) + { + // default if we cannot find a proof for a fundamental change + // the recurrence_event is the master event with start and end adjusted to the recurrence + // check for changed data + foreach (array('start','end','uid','title','location','description', + 'priority','public','special','non_blocking') as $key) + { + if (!empty($event[$key]) && $recurrence_event[$key] != $event[$key]) + { + if ($wasPseudo) + { + // We started with a pseudo exception + $type = 'SERIES-EXCEPTION-PROPAGATE'; + } + else + { + $type = 'SERIES-EXCEPTION'; + } + + if ($stored_event) + { + unset($stored_event['id']); // signal the true exception + $stored_event['recur_type'] = MCAL_RECUR_NONE; + } + break; + } + } + // the event id here is always the id of the master event + // unset it to prevent confusion of stored event and master event + unset($event['id']); + } + + // check ACL + if (is_array($master_event)) + { + $acl_edit = $this->check_perms(EGW_ACL_EDIT, $master_event['id']); + } + else + { + if (is_array($stored_event)) + { + $acl_edit = $this->check_perms(EGW_ACL_EDIT, $stored_event['id']); + } + else + { + $acl_edit = true; // new event + } + } + + return array( + 'type' => $type, + 'acl_edit' => $acl_edit, + 'stored_event' => $stored_event, + 'master_event' => $master_event, + ); + } + + /** + * Translates all timestamps for a given event from server-time to user-time. + * The update() and save() methods expect timestamps in user-time. + * @param &$event the event we are working on + * + */ + function server2usertime (&$event) + { + // we run all dates through date2usertime, to adjust to user-time + foreach(array('start','end','recur_enddate','recurrence') as $ts) + { + // we convert here from server-time to timestamps in user-time! + if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2usertime($event[$ts]) : 0; + } + // same with the recur exceptions + if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + { + foreach($event['recur_exception'] as $n => $date) + { + $event['recur_exception'][$n] = $this->date2usertime($date); + } + } + // same with the alarms + if (isset($event['alarm']) && is_array($event['alarm'])) + { + foreach($event['alarm'] as $id => $alarm) + { + $event['alarm'][$id]['time'] = $this->date2usertime($alarm['time']); + } + } + } +} diff --git a/phpgwapi/inc/class.contenthistory.inc.php b/phpgwapi/inc/class.contenthistory.inc.php index 61b373202c..0a85641282 100644 --- a/phpgwapi/inc/class.contenthistory.inc.php +++ b/phpgwapi/inc/class.contenthistory.inc.php @@ -58,31 +58,48 @@ class contenthistory * @param string$_appName the appname example: infolog_notes * @param string $_action can be modify, add or delete * @param string $_ts timestamp where to start searching from + * @param array $readableItems (optional) readable items of current user * @return array containing contentIds with changes */ - function getHistory($_appName, $_action, $_ts) + function getHistory($_appName, $_action, $_ts, $readableItems = false) { $where = array('sync_appname' => $_appName); - + $ts = $this->db->to_timestamp($_ts); + $idList = array(); + switch($_action) { case 'modify': - $where[] = "sync_modified > '".$this->db->to_timestamp($_ts)."' AND sync_deleted IS NULL"; + $where[] = "sync_modified > '".$ts."' AND sync_deleted IS NULL"; break; case 'delete': - $where[] = "sync_deleted > '".$this->db->to_timestamp($_ts)."'"; + $where[] = "sync_deleted > '".$ts."'"; break; case 'add': - $where[] = "sync_added > '".$this->db->to_timestamp($_ts)."' AND sync_deleted IS NULL AND sync_modified IS NULL"; + $where[] = "sync_added > '".$ts."' AND sync_deleted IS NULL AND sync_modified IS NULL"; break; default: // no valid $_action set return array(); } - $idList = array(); - foreach($this->db->select(self::TABLE,'sync_contentid',$where,__LINE__,__FILE__) as $row) + + if (is_array($readableItems)) { - $idList[] = $row['sync_contentid']; + foreach ($readableItems as $id) + { + $where['sync_contentid'] = $id; + if ($this->db->select(self::TABLE,'sync_contentid',$where,__LINE__,__FILE__)->fetchColumn()) + { + $idList[] = $id; + } + } + } + else + { + foreach ($this->db->select(self::TABLE,'sync_contentid',$where,__LINE__,__FILE__) as $row) + { + $idList[] = $row['sync_contentid']; + } } return $idList; @@ -165,9 +182,6 @@ class contenthistory 'sync_changedby' => $GLOBALS['egw_info']['user']['account_id'], $_action == 'delete' ? 'sync_deleted' : 'sync_modified' => $this->db->to_timestamp($_ts), ); - // if deleted entry get's modified, removed deleted timestamp, as it got recovered - if ($_action == 'modify') $newData['sync_deleted'] = null; - $this->db->update(self::TABLE, $newData, $where,__LINE__,__FILE__); break; } diff --git a/phpgwapi/inc/horde/Horde/SyncML/State_egw.php b/phpgwapi/inc/horde/Horde/SyncML/State_egw.php new file mode 100644 index 0000000000..5e07a73fdf --- /dev/null +++ b/phpgwapi/inc/horde/Horde/SyncML/State_egw.php @@ -0,0 +1,740 @@ + + * @author Joerg Lehrke + * @version $Id$ + */ + +include_once dirname(__FILE__).'/State.php'; + +/** + * The EGW_SyncML_State class provides a SyncML state object. + */ +class EGW_SyncML_State extends Horde_SyncML_State +{ + var $table_devinfo = 'egw_syncmldevinfo'; + + /* + * store the mappings of egw uids to client uids + */ + var $uidMappings = array(); + + /* + * the domain of the current user + */ + var $_account_domain = 'default'; + + /** + * get the local content id from a syncid + * + * @param sting $_syncid id used in syncml + * @return int local egw content id + */ + function get_egwID($_syncid) { + $syncIDParts = explode('-',$_syncid); + array_shift($syncIDParts); + $_id = implode ('', $syncIDParts); + return $_id; + } + + /** + * when got a entry last added/modified/deleted + * + * @param $_syncid containing appName-contentid + * @param $_action string can be add, delete or modify + * @return string the last timestamp + */ + function getSyncTSforAction($_syncid, $_action) { + $syncIDParts = explode('-',$_syncid); + $_appName = array_shift($syncIDParts); + $_id = implode ('', $syncIDParts); + + $ts = $GLOBALS['egw']->contenthistory->getTSforAction($_appName, $_id, $_action); + + if (strstr($_id, ':')) { + // pseudo entries are related to parent entry + $parentId = array_shift(explode(':', $_id)); + $pts = $GLOBALS['egw']->contenthistory->getTSforAction($_appName, $parentId, $_action); + if ($pts > $ts) $ts = $pts; // We have changed the parent + } + return $ts; + } + + /** + * get the timestamp for action + * + * find which content changed since $_ts for application $_appName + * + * @param string$_appName the appname example: infolog_notes + * @param string $_action can be modify, add or delete + * @param string $_ts timestamp where to start searching from + * @param array $readableItems (optional) readable items of current user + * @return array containing syncIDs with changes + */ + function getHistory($_appName, $_action, $_ts, $readableItems = false) { + $guidList = array(); + $syncIdList = array(); + $userItems = false; + if (is_array($readableItems)) + { + $userItems = array(); + foreach($readableItems as $guid) + { + if (preg_match('/'.$_appName.'-(\d+)(:(\d+))?/', $guid, $matches)) + { + // We use only the real items' ids + $userItems[] = $matches[1]; + } + } + $userItems = array_unique($userItems); + } + $idList = $GLOBALS['egw']->contenthistory->getHistory($_appName, $_action, $_ts, $userItems); + foreach ($idList as $idItem) + { + if ($idItem) // ignore inconsistent entries + { + $syncIdList[] = $_appName . '-' . $idItem; + } + } + return $syncIdList; + } + + /** + * Returns the timestamp (if set) of the last change to the + * obj:guid, that was caused by the client. This is stored to + * avoid mirroring these changes back to the client. + */ + function getChangeTS($type, $guid) { + $mapID = $this->_locName . $this->_sourceURI . $type; + + #Horde::logMessage('SyncML: getChangeTS for ' . $mapID + # . ' / '. $guid, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + if ($ts = $GLOBALS['egw']->db->select('egw_contentmap', 'map_timestamp', + array( + 'map_id' => $mapID, + 'map_guid' => $guid, + ), __LINE__, __FILE__, false, '', 'syncml')->fetchColumn()) { + #Horde::logMessage('SyncML: getChangeTS changets is ' + # . $GLOBALS['egw']->db->from_timestamp($ts), + # __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $GLOBALS['egw']->db->from_timestamp($ts); + } + return false; + } + + /** + * Returns the exceptions for a GUID which the client knows of + */ + function getGUIDExceptions($type, $guid) { + $mapID = $this->_locName . $this->_sourceURI . $type; + + #Horde::logMessage('SyncML: getChangeTS for ' . $mapID + # . ' / '. $guid, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $guid_exceptions = array(); + $where = array ('map_id' => $mapID,); + $where[] = "map_guid LIKE '$guid" . ":%'"; + + // Fetch all exceptions which the client knows of + foreach ($GLOBALS['egw']->db->select('egw_contentmap', 'map_guid', $where, + __LINE__,__FILE__, false, '', 'syncml') as $row) + { + $parts = preg_split('/:/', $row['map_guid']); + $Id = $parts[0]; + $extension = $parts[1]; + $guid_exceptions[$extension] = $row['map_guid']; + } + return $guid_exceptions; + } + + /** + * Retrieves information about the clients device info if any. Returns + * false if no info found or a DateTreeObject with at least the + * following attributes: + * + * a array containing all available infos about the device + */ + function getClientDeviceInfo() { + #Horde::logMessage("SyncML: getClientDeviceInfo " . $this->_locName + # . ", " . $this->_sourceURI, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $syncml_prefs = $GLOBALS['egw_info']['user']['preferences']['syncml']; + $deviceMaxEntries = 'maxEntries-' . $this->_clientDeviceInfo['devId']; + $deviceUIDExtension = 'uidExtension-' . $this->_clientDeviceInfo['devId']; + $deviceNonBlockingAllday = 'nonBlockingAllday-' . $this->_clientDeviceInfo['devId']; + $deviceTimezone = 'tzid-' . $this->_clientDeviceInfo['devId']; + $deviceCharSet = 'charset-' . $this->_clientDeviceInfo['devId']; + if (isset($this->_clientDeviceInfo) + && is_array($this->_clientDeviceInfo)) { + // update user preferences + $this->_clientDeviceInfo['maxEntries'] = $syncml_prefs[$deviceMaxEntries]; + $this->_clientDeviceInfo['uidExtension'] = $syncml_prefs[$deviceUIDExtension]; + $this->_clientDeviceInfo['nonBlockingAllday'] = $syncml_prefs[$deviceNonBlockingAllday]; + $this->_clientDeviceInfo['tzid'] = $syncml_prefs[$deviceTimezone]; + $this->_clientDeviceInfo['charset'] = $syncml_prefs[$deviceCharSet]; + // use cached information + return $this->_clientDeviceInfo; + } + if (!($deviceID = $GLOBALS['egw']->db->select('egw_syncmldeviceowner', + 'owner_devid', + array ( + 'owner_locname' => $this->_locName, + 'owner_deviceid' => $this->_sourceURI, + ), __LINE__, __FILE__, false, '', 'syncml')->fetchColumn())) { + return false; + } + + $cols = array( + 'dev_dtdversion', + 'dev_numberofchanges', + 'dev_largeobjs', + 'dev_swversion', + 'dev_fwversion', + 'dev_hwversion', + 'dev_oem', + 'dev_model', + 'dev_manufacturer', + 'dev_devicetype', + 'dev_datastore', + 'dev_utc', + ); + + #Horde::logMessage("SyncML: getClientDeviceInfo $deviceID", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $where = array( + 'dev_id' => $deviceID, + ); + + + + if (($row = $GLOBALS['egw']->db->select('egw_syncmldevinfo', + $cols, $where, __LINE__, __FILE__, false, '', 'syncml')->fetch())) { + $this->_clientDeviceInfo = array ( + 'DTDVersion' => $row['dev_dtdversion'], + 'supportNumberOfChanges' => $row['dev_numberofchanges'], + 'supportLargeObjs' => $row['dev_largeobjs'], + 'UTC' => $row['dev_utc'], + 'softwareVersion' => $row['dev_swversion'], + 'hardwareVersion' => $row['dev_hwversion'], + 'firmwareVersion' => $row['dev_fwversion'], + 'oem' => $row['dev_oem'], + 'model' => $row['dev_model'], + 'manufacturer' => $row['dev_manufacturer'], + 'deviceType' => $row['dev_devicetype'], + 'maxMsgSize' => $this->_maxMsgSize, + 'maxEntries' => $syncml_prefs[$deviceMaxEntries], + 'uidExtension' => $syncml_prefs[$deviceUIDExtension], + 'nonBlockingAllday' => $syncml_prefs[$deviceNonBlockingAllday], + 'tzid' => $syncml_prefs[$deviceTimezone], + 'charset' => $syncml_prefs[$deviceCharSet], + 'devId' => $deviceID, + 'dataStore' => unserialize($row['dev_datastore']), + ); + return $this->_clientDeviceInfo; + } + return false; + } + + /** + * returns GUIDs of all client items + */ + function getClientItems($type=false) { + if (!$type) $type = $this->_targetURI; + $mapID = $this->_locName . $this->_sourceURI . $type; + + $guids = array(); + foreach($GLOBALS['egw']->db->select('egw_contentmap', 'map_guid', array( + 'map_id' => $mapID, + 'map_expired' => false, + ), __LINE__, __FILE__, false, '', 'syncml') as $row) { + $guids[] = $row['map_guid']; + } + return $guids; + } + + /** + * Retrieves the Horde server guid (like + * kronolith:0d1b415fc124d3427722e95f0e926b75) for a given client + * locid. Returns false if no such id is stored yet. + * + * Opposite of getLocId which returns the locid for a given guid. + */ + function getGlobalUID($type, $locid) { + $mapID = $this->_locName . $this->_sourceURI . $type; + + #Horde::logMessage('SyncML: search GlobalUID for ' . $mapID .' / '.$locid, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + return $GLOBALS['egw']->db->select('egw_contentmap', 'map_guid', + array( + 'map_id' => $mapID, + 'map_locuid' => $locid, + 'map_expired' => false, + ), __LINE__, __FILE__, false, '', 'syncml')->fetchColumn(); + } + + /** + * Converts a EGW GUID (like + * kronolith:0d1b415fc124d3427722e95f0e926b75) to a client ID as + * used by the sync client (like 12) returns false if no such id + * is stored yet. + */ + function getLocID($type, $guid) { + $mapID = $this->_locName . $this->_sourceURI . $type; + + Horde::logMessage('SyncML: search LocID for ' . $mapID . ' / ' . $guid, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + if (($locuid = $GLOBALS['egw']->db->select('egw_contentmap', 'map_locuid', array( + 'map_id' => $mapID, + 'map_guid' => $guid + ), __LINE__, __FILE__, false, '', 'syncml')->fetchColumn())) { + Horde::logMessage('SyncML: found LocID: '.$locuid, __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + return $locuid; + } + + /** + * Retrieves information about the previous sync if any. Returns + * false if no info found or a DateTreeObject with at least the + * following attributes: + * + * ClientAnchor: the clients Next Anchor of the previous sync. + * ServerAnchor: the Server Next Anchor of the previous sync. + */ + function getSyncSummary($type) { + $deviceID = $this->_locName . $this->_sourceURI; + + Horde::logMessage("SyncML: getSyncSummary for $deviceID", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + if (($row = $GLOBALS['egw']->db->select('egw_syncmlsummary', array('sync_serverts','sync_clientts'), array( + 'dev_id' => $deviceID, + 'sync_path' => $type + ), __LINE__, __FILE__, false, '', 'syncml')->fetch())) { + Horde::logMessage("SyncML: getSyncSummary for $deviceID serverts: ".$row['sync_serverts']." clients: ".$row['sync_clientts'], __FILE__, __LINE__, PEAR_LOG_DEBUG); + return array( + 'ClientAnchor' => $row['sync_clientts'], + 'ServerAnchor' => $row['sync_serverts'], + ); + } + return false; + } + + function isAuthorized() { + + if(!isset($this->_locName)) + { + Horde::logMessage('SyncML: Authentication not yet possible. Username not available', + __FILE__, __LINE__, PEAR_LOG_DEBUG); + return false; + } + + // store sessionID in a variable, because create() and verify() reset this value + $sessionID = session_id(); + + if (strpos($this->_locName,'@') === False) + { + $this->_account_domain = $GLOBALS['egw_info']['server']['default_domain']; + $this->_locName .= '@'. ($this->_account_domain ? $this->_account_domain : 'default'); + } + else + { + $parts = explode('@',$this->_locName); + $this->_account_domain = array_pop($parts); + } + + if (!is_object($GLOBALS['egw'])) + { + // Let the EGw core create the infrastructure classes + $_POST['login'] = $this->_locName; + $_REQUEST['domain'] = $this->_account_domain; + $GLOBALS['egw_info']['server']['default_domain'] = $this->_account_domain; + $GLOBALS['egw_info']['user']['domain'] = $this->_account_domain; + $GLOBALS['egw_info']['flags']['currentapp'] = 'login'; + $GLOBALS['egw_info']['flags']['noapi'] = false; + require_once(EGW_API_INC . '/functions.inc.php'); + } + + $GLOBALS['egw_info']['flags']['currentapp'] = 'syncml'; + + if (!$this->_isAuthorized) + { + + if (!isset($this->_password)) + { + Horde::logMessage('SyncML: Authentication not yet possible. Credetials missing', + __FILE__, __LINE__, PEAR_LOG_DEBUG); + return false; + } + + if ($GLOBALS['egw']->session->create($this->_locName,$this->_password,'text')) + { + if ($GLOBALS['egw_info']['user']['apps']['syncml']) + { + $this->_isAuthorized = 1; + // restore the original sessionID + session_regenerate_id(); + session_id($sessionID); + $GLOBALS['sessionid'] = $sessionID; + @session_start(); + Horde::logMessage('SyncML_EGW[' . $GLOBALS['sessionid'] + .']: Authentication of ' . $this->_locName . ' succeded', + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $syncml_prefs = $GLOBALS['egw_info']['user']['preferences']['syncml']; + if (($deviceID = $GLOBALS['egw']->db->select('egw_syncmldeviceowner', + 'owner_devid', + array ( + 'owner_locname' => $this->_locName, + 'owner_deviceid' => $this->_sourceURI, + ), __LINE__, __FILE__, false, '', 'syncml')->fetchColumn())) { + $allowed_name = 'allowed-' . $deviceID; + if (isset($syncml_prefs[$allowed_name])) + { + $deviceAllowed = $syncml_prefs[$allowed_name]; + } + else + { + $deviceAllowed = -1; + } + } + else + { + $deviceAllowed = -1; // Unkown device + } + if (!$GLOBALS['egw_info']['user']['apps']['admin'] && + isset($syncml_prefs['deny_unknown_devices']) && + $syncml_prefs['deny_unknown_devices'] != 0) + { + if ($syncml_prefs['deny_unknown_devices'] == -1 && + $deviceAllowed != 1 || + $syncml_prefs['deny_unknown_devices'] == 1 && + $deviceAllowed == 0) + { + $this->_isAuthorized = -1; + Horde::logMessage('SyncML: Device is not allowed for user ' . $this->_locName, + __FILE__, __LINE__, PEAR_LOG_INFO); + } + } + } + else + { + $this->_isAuthorized = -1; // Authorization failed! + Horde::logMessage('SyncML is not enabled for user ' + . $this->_locName, __FILE__, __LINE__, PEAR_LOG_ERROR); + } + } + else + { + $this->_isAuthorized = -1; + Horde::logMessage('SyncML: Authentication of ' . $this->_locName + . ' failed', __FILE__, __LINE__, PEAR_LOG_INFO); + + } + + } + elseif ($this->_isAuthorized > 0) + { + if (!$GLOBALS['egw']->session->verify($sessionID, 'staticsyncmlkp3')) + { + Horde::logMessage('SyncML_EGW: egw session(' . $sessionID + . ') could not be not verified' , + __FILE__, __LINE__, PEAR_LOG_ERROR); + } + if (empty($GLOBALS['egw_info']['user']['passwd'])) + { + $GLOBALS['egw_info']['user']['passwd'] = $this->_password; + } + } + return ($this->_isAuthorized > 0); + } + + /** + * Removes all locid<->guid mappings for the given type. + * Returns always true. + */ + function removeAllUID($type) { + $mapID = $this->_locName . $this->_sourceURI . $type; + + Horde::logMessage("SyncML: state->removeAllUID(type=$type)", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $GLOBALS['egw']->db->delete('egw_contentmap', array('map_id' => $mapID), __LINE__, __FILE__, 'syncml'); + + return true; + } + + /** + * Used in SlowSync + * Removes all locid<->guid mappings for the given type, + * that are older than $ts. + * + * Returns always true. + */ + function removeOldUID($type, $ts) { + $mapID = $this->_locName . $this->_sourceURI . $type; + $where[] = "map_id = '".$mapID."' AND map_timestamp < '".$GLOBALS['egw']->db->to_timestamp($ts)."'"; + + Horde::logMessage("SyncML: state->removeOldUID(type=$type)", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $GLOBALS['egw']->db->delete('egw_contentmap', $where, __LINE__, __FILE__, 'syncml'); + + return true; + } + + /** + * Used at session end to cleanup expired entries + * Removes all locid<->guid mappings for the given type, + * that are marked as expired and older than $ts. + * + * Returns always true. + */ + function removeExpiredUID($type, $ts) { + $mapID = $this->_locName . $this->_sourceURI . $type; + $where['map_id'] = $mapID; + $where['map_expired'] = true; + $where[] = "map_timestamp <= '".$GLOBALS['egw']->db->to_timestamp($ts)."'"; + + Horde::logMessage("SyncML: state->removeExpiredUID(type=$type)", + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $GLOBALS['egw']->db->delete('egw_contentmap', $where, + __LINE__, __FILE__, 'syncml'); + + return true; + } + + /** + * Check if an entry is already expired + * + * Returns true for expired mappings. + */ + function isExpiredUID($type, $locid) { + $mapID = $this->_locName . $this->_sourceURI . $type; + $expired = false; + $where = array( + 'map_id' => $mapID, + 'map_locuid' => $locid, + ); + if (($expired = $GLOBALS['egw']->db->select('egw_contentmap', 'map_expired', + $where, __LINE__, __FILE__, false, '', 'syncml')->fetchColumn())) { + Horde::logMessage('SyncML: found LocID: '. $locid, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + return $expired; + } + + /** + * Removes the locid<->guid mapping for the given locid. Returns + * the guid that was removed or false if no mapping entry was + * found. + */ + function removeUID($type, $locid) { + $mapID = $this->_locName . $this->_sourceURI . $type; + + $where = array ( + 'map_id' => $mapID, + 'map_locuid' => $locid + ); + + if (!($guid = $GLOBALS['egw']->db->select('egw_contentmap', 'map_guid', $where, + __LINE__, __FILE__, false, '', 'syncml')->fetchColumn())) { + Horde::logMessage("SyncML: state->removeUID(type=$type,locid=$locid)" + . " nothing to remove", __FILE__, __LINE__, PEAR_LOG_INFO); + return false; + } + + Horde::logMessage("SyncML: state->removeUID(type=$type,locid=$locid): " + . "removing guid $guid", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $GLOBALS['egw']->db->delete('egw_contentmap', $where, __LINE__, __FILE__, 'syncml'); + + return $guid; + } + + /** + * Puts a given client $locid and Horde server $guid pair into the + * map table to allow mapping between the client's and server's + * IDs. Actually there are two maps: from the localid to the guid + * and vice versa. The localid is converted to a key as follows: + * this->_locName . $this->_sourceURI . $type . $locid so you can + * have different syncs with different devices. If an entry + * already exists, it is overwritten. + * Expired entries can be deleted at the next session start. + */ + function setUID($type, $locid, $_guid, $ts=0, $expired=false) { + #Horde::logMessage("SyncML: setUID $type, $locid, $_guid, $ts ", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + if (!strlen("$_guid")) { + // We can't handle this case otherwise + return; + } + + // problem: entries created from client, come here with the (long) server guid, + // but getUIDMapping does not know them and can not map server-guid <--> client guid + $guid = $this->getUIDMapping($_guid); + if($guid === false) { + // this message is not really usefull here because setUIDMapping is only called when adding content to the client, + // however setUID is called also when adding content from the client. So in all other conditions this + // message will be logged. + //Horde::logMessage("SyncML: setUID $type, $locid, $guid something went wrong!!! Mapping not found.", __FILE__, __LINE__, PEAR_LOG_INFO); + $guid = $_guid; + //return false; + } + #Horde::logMessage("SyncML: setUID $_guid => $guid", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + if(!$ts) $ts = time(); + + Horde::logMessage("SyncML: setUID $type, $locid, $guid, $ts ", + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $mapID = $this->_locName . $this->_sourceURI . $type; + + // expire all client id's + $where = array( + 'map_id' => $mapID, + 'map_locuid' => $locid, + ); + $GLOBALS['egw']->db->delete('egw_contentmap', $where, + __LINE__, __FILE__, 'syncml'); + + // expire old EGw id's + $where = array( + 'map_id' => $mapID, + 'map_guid' => $guid, + ); + $GLOBALS['egw']->db->delete('egw_contentmap', $where, + __LINE__, __FILE__, 'syncml'); + /* + $data = array ('map_expired' => true); + $GLOBALS['egw']->db->update('egw_contentmap', $data, $where, + __LINE__, __FILE__, 'syncml'); + */ + $data = $where + array( + 'map_locuid' => $locid, + 'map_timestamp' => $ts, + 'map_expired' => ($expired ? true : false), + ); + $GLOBALS['egw']->db->insert('egw_contentmap', $data, $where, + __LINE__, __FILE__, 'syncml'); + } + + /** + * writes clients deviceinfo into database + */ + function writeClientDeviceInfo() { + if (!isset($this->_clientDeviceInfo) + || !is_array($this->_clientDeviceInfo)) { + return false; + } + + if(!isset($this->size_dev_hwversion)) { + $tableDefDevInfo = $GLOBALS['egw']->db->get_table_definitions('syncml',$this->table_devinfo); + $this->size_dev_hwversion = $tableDefDevInfo['fd']['dev_hwversion']['precision']; + unset($tableDefDevInfo); + } + + $softwareVersion = !empty($this->_clientDeviceInfo['softwareVersion']) ? $this->_clientDeviceInfo['softwareVersion'] : ''; + $hardwareVersion = !empty($this->_clientDeviceInfo['hardwareVersion']) ? substr($this->_clientDeviceInfo['hardwareVersion'], 0, $this->size_dev_hwversion) : ''; + $firmwareVersion = !empty($this->_clientDeviceInfo['firmwareVersion']) ? $this->_clientDeviceInfo['firmwareVersion'] : ''; + + $where = array( + 'dev_model' => $this->_clientDeviceInfo['model'], + 'dev_manufacturer' => $this->_clientDeviceInfo['manufacturer'], + 'dev_swversion' => $softwareVersion, + 'dev_hwversion' => $hardwareVersion, + 'dev_fwversion' => $firmwareVersion, + ); + + if (($deviceID = $GLOBALS['egw']->db->select('egw_syncmldevinfo', 'dev_id', $where, + __LINE__, __FILE__, false, '', 'syncml')->fetchColumn())) { + $data = array ( + 'dev_datastore' => serialize($this->_clientDeviceInfo['dataStore']), + ); + $GLOBALS['egw']->db->update('egw_syncmldevinfo', $data, $where, + __LINE__, __FILE__, 'syncml'); + } else { + $data = array ( + 'dev_dtdversion' => $this->_clientDeviceInfo['DTDVersion'], + 'dev_numberofchanges' => ($this->_clientDeviceInfo['supportNumberOfChanges'] ? true : false), + 'dev_largeobjs' => ($this->_clientDeviceInfo['supportLargeObjs'] ? true : false), + 'dev_utc' => ($this->_clientDeviceInfo['UTC'] ? true : false), + 'dev_swversion' => $softwareVersion, + 'dev_hwversion' => $hardwareVersion, + 'dev_fwversion' => $firmwareVersion, + 'dev_oem' => $this->_clientDeviceInfo['oem'], + 'dev_model' => $this->_clientDeviceInfo['model'], + 'dev_manufacturer' => $this->_clientDeviceInfo['manufacturer'], + 'dev_devicetype' => $this->_clientDeviceInfo['deviceType'], + 'dev_datastore' => serialize($this->_clientDeviceInfo['dataStore']), + ); + $GLOBALS['egw']->db->insert('egw_syncmldevinfo', $data, $where, __LINE__, __FILE__, 'syncml'); + + $deviceID = $GLOBALS['egw']->db->get_last_insert_id('egw_syncmldevinfo', 'dev_id'); + } + + $where = array ( + 'owner_locname' => $this->_locName, + 'owner_deviceid' => $this->_sourceURI, + ); + + if ($GLOBALS['egw']->db->select('egw_syncmldeviceowner', 'owner_devid', $where, + __LINE__, __FILE__, false, '', 'syncml')->fetchColumn()) { + $data = array ( + 'owner_devid' => $deviceID, + ); + $GLOBALS['egw']->db->update('egw_syncmldeviceowner', $data, $where, + __LINE__, __FILE__, 'syncml'); + } else { + $data = array ( + 'owner_locname' => $this->_locName, + 'owner_deviceid' => $this->_sourceURI, + 'owner_devid' => $deviceID, + ); + $GLOBALS['egw']->db->insert('egw_syncmldeviceowner', $data, $where, + __LINE__, __FILE__, 'syncml'); + } + } + + /** + * After a successful sync, the client and server's Next Anchors + * are written to the database so they can be used to negotiate + * upcoming syncs. + */ + function writeSyncSummary() { + #parent::writeSyncSummary(); + + if (!isset($this->_serverAnchorNext) + || !is_array($this->_serverAnchorNext)) { + return; + } + + $deviceID = $this->_locName . $this->_sourceURI; + + foreach((array)$this->_serverAnchorNext as $type => $a) { + Horde::logMessage("SyncML: write SYNCSummary for $deviceID " + . "$type serverts: $a clients: " + . $this->_clientAnchorNext[$type], + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $where = array( + 'dev_id' => $deviceID, + 'sync_path' => $type, + ); + + $data = array( + 'sync_serverts' => $a, + 'sync_clientts' => $this->_clientAnchorNext[$type] + ); + + $GLOBALS['egw']->db->insert('egw_syncmlsummary', $data, $where, + __LINE__, __FILE__, 'syncml'); + } + } +}