diff --git a/infolog/inc/class.boinfolog.inc.php b/infolog/inc/class.boinfolog.inc.php index de789586fa..08678c5117 100644 --- a/infolog/inc/class.boinfolog.inc.php +++ b/infolog/inc/class.boinfolog.inc.php @@ -227,8 +227,9 @@ class boinfolog extends infolog_bo /** * Convert an InfoLog entry into its xmlrpc representation, eg. convert timestamps to datetime.iso8601 * - * @param array $data infolog entry - * @param array xmlrpc infolog entry + * @param array $data infolog entry in db format + * + * @return array xmlrpc infolog entry */ function data2xmlrpc($data) { @@ -272,8 +273,9 @@ class boinfolog extends infolog_bo /** * Convert an InfoLog xmlrpc representation into the internal one, eg. convert datetime.iso8601 to timestamps * - * @param array $data infolog entry - * @param array xmlrpc infolog entry + * @param array $data infolog entry in xmlrpc representation + * + * @return array infolog entry in db format */ function xmlrpc2data($data) { diff --git a/infolog/inc/class.infolog_bo.inc.php b/infolog/inc/class.infolog_bo.inc.php index 9c2d95f474..0ac6f88873 100644 --- a/infolog/inc/class.infolog_bo.inc.php +++ b/infolog/inc/class.infolog_bo.inc.php @@ -30,16 +30,25 @@ class infolog_bo var $vfs_basedir='/infolog'; var $link_pathes = array(); var $send_file_ips = array(); - - var $tz_offset = 0; /** - * offset in secconds between user and server-time, - * it need to be add to a server-time to get the user-time or substracted from a user-time to get the server-time + * Set Logging + * + * @var boolean + */ + var $log = false; + /** + * Cached timezone data + * + * @var array id => data + */ + protected static $tz_cache = array(); + /** + * current time as timestamp in user-time and server-time * * @var int */ - var $tz_offset_s = 0; var $user_time_now; + var $now; /** * name of timestamps in an InfoLog entry * @@ -233,9 +242,8 @@ class infolog_bo $this->user = $GLOBALS['egw_info']['user']['account_id']; - $this->tz_offset = $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; - $this->tz_offset_s = 60*60*$this->tz_offset; - $this->user_time_now = time() + $this->tz_offset_s; + $this->now = time(); + $this->user_time_now = egw_time::server2user($this->now,'ts'); $this->grants = $GLOBALS['egw']->acl->get_grants('infolog',$this->group_owners ? $this->group_owners : true); $this->so = new infolog_so($this->grants); @@ -275,7 +283,7 @@ class infolog_bo /** * check's if user has the requiered rights on entry $info_id * - * @param int/array $info data or info_id of infolog entry to check + * @param int|array $info data or info_id of infolog entry to check * @param int $required_rights EGW_ACL_{READ|EDIT|ADD|DELETE} * @return boolean */ @@ -405,15 +413,111 @@ class infolog_bo return substr($des,0,60).' ...'; } + /** + * Convert the timestamps from given timezone to another and keep dates. + * The timestamps are mostly expected to be in server-time + * and $fromTZId is only used to qualify dates. + * + * @param array $values to modify + * @param string $fromTZId=null + * @param string $toTZId=false + * TZID timezone name e.g. 'UTC' + * or NULL for timestamps in user-time + * or false for timestamps in server-time + */ + function time2time(&$values, $fromTZId=false, $toTZId=null) + { + + if ($fromTZId === $toTZId) return; + + $tz = egw_time::$server_timezone; + + if ($fromTZId) + { + if (!isset(self::$tz_cache[$fromTZId])) + { + self::$tz_cache[$fromTZId] = calendar_timezones::DateTimeZone($fromTZId); + } + $fromTZ = self::$tz_cache[$fromTZId]; + } + elseif (is_null($fromTZId)) + { + $tz = egw_time::$user_timezone; + $fromTZ = egw_time::$user_timezone; + } + else + { + $fromTZ = egw_time::$server_timezone; + } + if ($toTZId) + { + if (!isset(self::$tz_cache[$toTZId])) + { + self::$tz_cache[$toTZId] = calendar_timezones::DateTimeZone($toTZId); + } + $toTZ = self::$tz_cache[$toTZId]; + } + elseif (is_null($toTZId)) + { + $toTZ = egw_time::$user_timezone; + } + else + { + $toTZ = egw_time::$server_timezone; + } + + foreach($this->timestamps as $key) + { + if ($values[$key]) + { + $time = new egw_time($values[$key], $tz); + $time->setTimezone($fromTZ); + if ($key == 'info_enddate') + { + // Set due date to 00:00 + $time->setTime(0, 0, 0); + } + if ($time->format('Hi') == '0000') + { + // we keep dates the same in new timezone + $arr = egw_time::to($time,'array'); + $time = new egw_time($arr, $toTZ); + } + else + { + $time->setTimezone($toTZ); + } + $values[$key] = egw_time::to($time,'ts'); + } + } + } + + /** + * convert a date from server to user-time + * + * @param int $ts timestamp in server-time + * @param string $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format + * @return mixed depending of $date_format + */ + function date2usertime($ts,$date_format='ts') + { + if (empty($ts) || $date_format == 'server') return $ts; + + return egw_time::server2user($ts,$date_format); + } + /** * Read an infolog entry specified by $info_id * - * @param int/array $info_id integer id or array with key 'info_id' of the entry to read + * @param int|array $info_id integer id or array with key 'info_id' of the entry to read * @param boolean $run_link_id2from=true should link_id2from run, default yes, * need to be set to false if called from link-title to prevent an infinit recursion - * @return array/boolean infolog entry, null if not found or false if no permission to read it + * @param string $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, + * 'array'=array or string with date-format + * + * @return array|boolean infolog entry, null if not found or false if no permission to read it */ - function &read($info_id,$run_link_id2from=true) + function &read($info_id,$run_link_id2from=true,$date_format='ts') { if (is_array($info_id)) { @@ -437,13 +541,21 @@ class infolog_bo } if ($run_link_id2from) $this->link_id2from($data); - // convert system- to user-time - foreach($this->timestamps as $time) + // convert server- to user-time + if ($date_format == 'ts') { - if ($data[$time]) $data[$time] += $this->tz_offset_s; + $this->time2time($data); + + // pre-cache title and file access + self::set_link_cache($data); + } + else + { + $time = new egw_time($data['info_enddate'], egw_time::$server_timezone); + // Set due date to 00:00 + $time->setTime(0, 0, 0); + $data['info_enddate'] = egw_time::to($time,'ts'); } - // pre-cache title and file access - self::set_link_cache($data); return $data; } @@ -451,9 +563,9 @@ class infolog_bo /** * Delete an infolog entry, evtl. incl. it's children / subs * - * @param int/array $info_id int id + * @param int|array $info_id int id * @param boolean $delete_children should the children be deleted - * @param int/boolean $new_parent parent to use for not deleted children if > 0 + * @param int|boolean $new_parent parent to use for not deleted children if > 0 * @return boolean True if delete was successful, False otherwise ($info_id does not exist or no rights) */ function delete($info_id,$delete_children=False,$new_parent=False) @@ -488,7 +600,7 @@ class infolog_bo } } } - if (!($info = $this->read($info_id))) return false; // should not happen + if (!($info = $this->read($info_id, true, 'server'))) return false; // should not happen $deleted = $info; $deleted['info_status'] = 'deleted'; @@ -532,9 +644,11 @@ class infolog_bo * @param array &$values values to write * @param boolean $check_defaults=true check and set certain defaults * @param boolean $touch_modified=true touch the modification data and sets the modiefier's user-id + * @param boolean $user2server=true conversion between user- and server-time necessary + * * @return int/boolean info_id on a successfull write or false */ - function write(&$values, $check_defaults=True, $touch_modified=True) + function write(&$values, $check_defaults=true, $touch_modified=true, $user2server=true) { //echo "boinfolog::write()values="; _debug_array($values); if ($status_only = $values['info_id'] && !$this->check_access($values['info_id'],EGW_ACL_EDIT)) @@ -572,13 +686,13 @@ class infolog_bo 'info_id' => $values['info_id'], 'info_datemodified' => $values['info_datemodified'], ); - foreach($this->responsible_edit as $name) + foreach ($this->responsible_edit as $name) { if (isset($backup_values[$name])) $values[$name] = $backup_values[$name]; } if ($set_completed) { - $values['info_datecompleted'] = $this->user_time_now; + $values['info_datecompleted'] = $user2server ? $this->user_time_now : $this->now; $values['info_percent'] = 100; $forcestatus = true; $status = 'done'; @@ -605,7 +719,7 @@ class infolog_bo if (!$values['info_datecompleted'] && (in_array($values['info_status'],array('done','billed')) || (int)$values['info_percent'] == 100)) { - $values['info_datecompleted'] = $this->user_time_now; // set date completed to today if status == done + $values['info_datecompleted'] = $user2server ? $this->user_time_now : $this->now; // set date completed to today if status == done } if (in_array($values['info_status'],array('done','billed'))) { @@ -651,44 +765,64 @@ class infolog_bo { $values['info_owner'] = $this->so->user; } + if ($info_from_set = ($values['info_link_id'] && isset($values['info_from']) && empty($values['info_from']))) { $values['info_from'] = $this->link_id2from($values); } + + if ($status_only && !$undelete) $values = array_merge($backup_values,$values); + + $to_write = $values; + if ($user2server) + { + // convert user- to server-time + $this->time2time($to_write, null, false); + $time = new egw_time($values['info_enddate'], egw_time::$user_timezone); + // Set due date to 00:00 + $time->setTime(0, 0, 0); + $values['info_enddate'] = egw_time::to($time,'ts'); + } + else + { + $time = new egw_time($values['info_enddate'], egw_time::$server_timezone); + // Set due date to 00:00 + $time->setTime(0, 0, 0); + $to_write['info_enddate'] = egw_time::to($time,'ts'); + // convert server- to user-time + $this->time2time($values); + } + if ($touch_modified || !$values['info_datemodified']) { // Should only an entry be updated which includes the original modification date? // Used in the web-GUI to check against a modification by an other user while editing the entry. // It's now disabled for xmlrpc, as otherwise the xmlrpc code need to be changed! $xmlrpc = is_object($GLOBALS['server']) && $GLOBALS['server']->last_method; - $check_modified = $values['info_datemodified'] && !$xmlrpc ? $values['info_datemodified']-$this->tz_offset_s : false; + $check_modified = $values['info_datemodified'] && !$xmlrpc ? $to_write['info_datemodified'] : false; $values['info_datemodified'] = $this->user_time_now; + $to_write['info_datemodified'] = $this->now; } if ($touch_modified || !$values['info_modifier']) { $values['info_modifier'] = $this->so->user; + $to_write['info_modifier'] = $this->so->user; } //_debug_array($values); // error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($values)."\n",3,'/tmp/infolog'); - $to_write = $values; - if ($status_only && !$undelete) $values = array_merge($backup_values,$values); - // convert user- to system-time - foreach($this->timestamps as $time) - { - if ($to_write[$time]) $to_write[$time] -= $this->tz_offset_s; - } + // we need to get the old values to update the links in customfields and for the tracking if ($values['info_id']) { - $old = $this->read($values['info_id'],false); + $old = $this->read($values['info_id'], false, 'server'); } - if(($info_id = $this->so->write($to_write,$check_modified))) + if (($info_id = $this->so->write($to_write,$check_modified))) { if (!isset($values['info_type']) || $status_only) { - $values = $this->read($info_id); + $values = $this->read($info_id, true, 'server'); } - if($values['info_id'] && $old['info_status'] != 'deleted') + if ($values['info_id'] && $old['info_status'] != 'deleted') { // update $GLOBALS['egw']->contenthistory->updateTimeStamp( @@ -705,11 +839,13 @@ class infolog_bo ); } $values['info_id'] = $info_id; + $to_write['info_id'] = $info_id; // if the info responbsible array is not passed, fetch it from old. if (!array_key_exists('info_responsible',$values)) $values['info_responsible'] = $old['info_responsible']; if (!is_array($values['info_responsible'])) // this should not happen, bug it does ;-) { $values['info_responsible'] = $values['info_responsible'] ? explode(',',$values['info_responsible']) : array(); + $to_write['info_responsible'] = $values['info_responsible']; } // create (and remove) links in custom fields customfields_widget::update_customfield_links('infolog',$values,$old,'info_id'); @@ -729,8 +865,9 @@ class infolog_bo if ($old && ($missing_fields = array_diff_key($old,$values))) { $values = array_merge($values,$missing_fields); + $to_write = array_merge($to_write,$missing_fields); } - $this->tracking->track($values,$old,$this->user,$values['info_status'] == 'deleted' || $old['info_status'] == 'deleted'); + $this->tracking->track($to_write,$old,$this->user,$values['info_status'] == 'deleted' || $old['info_status'] == 'deleted'); } if ($info_from_set) $values['info_from'] = ''; @@ -764,18 +901,40 @@ class infolog_bo function &search(&$query) { //echo "

boinfolog::search(".print_r($query,True).")

\n"; + if (!empty($query['start'])) + { + $query['start'] = egw_time::user2server($query['start'],'ts'); + } + $ret = $this->so->search($query); - // convert system- to user-time if (is_array($ret)) { - foreach($ret as $id => &$data) + foreach ($ret as $id => &$data) { - if($this->tz_offset_s) + if (!$this->check_access($data,EGW_ACL_READ)) { - foreach($this->timestamps as $time) + unset($ret[$id]); + continue; + } + // convert system- to user-time + foreach ($this->timestamps as $key) + { + if ($data[$key]) { - if ($data[$time]) $data[$time] += $this->tz_offset_s; + $time = new egw_time($data[$key], egw_time::$server_timezone); + if ($key == 'info_enddate') $time->setTime(0, 0,0 ); // Set due date to 00:00 + if ($time->format('Hi') == '0000') + { + // we keep dates the same in user-time + $arr = egw_time::to($time,'array'); + $time = new egw_time($arr, egw_time::$user_timezone); + } + else + { + $time->setTimezone(egw_time::$user_timezone); + } + $data[$key] = egw_time::to($time,'ts'); } } // pre-cache title and file access @@ -880,10 +1039,10 @@ class infolog_bo * * Is called as hook to participate in the linking * - * @param int/array $info int info_id or array with infolog entry - * @return string/boolean string with the title, null if $info not found, false if no perms to view + * @param int|array $info int info_id or array with infolog entry + * @return string|boolean string with the title, null if $info not found, false if no perms to view */ - function link_title( $info ) + function link_title($info) { if (!is_array($info)) { @@ -902,16 +1061,16 @@ class infolog_bo * * @param array $ids */ - function link_titles( array $ids ) + function link_titles(array $ids) { $titles = array(); - foreach($this->search($params=array( + foreach ($this->search($params=array( 'col_filter' => array('info_id' => $ids), )) as $info) { $titles[$info['info_id']] = $this->link_title($info); } - foreach(array_diff($ids,array_keys($titles)) as $id) + foreach (array_diff($ids,array_keys($titles)) as $id) { $titles[$id] = false; // we assume every not returned entry to be not readable, as we notify the link class about all deletes } @@ -927,7 +1086,7 @@ class infolog_bo * @param array $options Array of options for the search * @return array with info_id - title pairs of the matching entries */ - function link_query( $pattern, Array &$options = array() ) + function link_query($pattern, Array &$options = array()) { $query = array( 'search' => $pattern, @@ -940,7 +1099,7 @@ class infolog_bo $content = array(); if (is_array($ids)) { - foreach($ids as $id => $info ) + foreach ($ids as $id => $info ) { $content[$id] = $this->link_title($id); } @@ -1010,10 +1169,11 @@ class infolog_bo } while ($infos = $this->search($query)) { - foreach($infos as $info) + foreach ($infos as $info) { - $time = (int) adodb_date('Hi',$info['info_startdate']); - $date = adodb_date('Y/m/d',$info['info_startdate']); + $start = new egw_time($info['info_startdate'],egw_time::$user_timezone); + $time = (int) $start->format('Hi'); + $date = $start->format('Y/m/d'); /* As event-like infologs are not showen in current calendar, we need to present all open infologs to the user! (2006-06-27 nelius) if ($do_events && !$time || @@ -1021,11 +1181,11 @@ class infolog_bo { continue; }*/ - $title = ($do_events?$GLOBALS['egw']->common->formattime(adodb_date('H',$info['info_startdate']),adodb_date('i',$info['info_startdate'])).' ':''). + $title = ($do_events?common::formattime($start->format('H'),$start->format('i')).' ':''). $info['info_subject']; $view = egw_link::view('infolog',$info['info_id']); $content=array(); - foreach($icons = array( + foreach ($icons = array( $info['info_type'] => 'infolog', $this->status[$info['info_type']][$info['info_status']] => 'infolog', ) as $name => $app) @@ -1064,7 +1224,7 @@ class infolog_bo if (isset($args['infolog']) && count($args['infolog'])) { $icons = $this->so->get_status($args['infolog']); - foreach((array) $icons as $id => $status) + foreach ((array) $icons as $id => $status) { if ($status && substr($status,-1) != '%') { @@ -1092,18 +1252,17 @@ class infolog_bo { $this->categories = new categories($this->user,'infolog'); } - - if($info_id && $info_id > 0) + $old_cats_preserve = array(); + if ($info_id && $info_id > 0) { // preserve categories without users read access $old_infolog = $this->read($info_id); $old_categories = explode(',',$old_infolog['info_cat']); - $old_cats_preserve = array(); - if(is_array($old_categories) && count($old_categories) > 0) + if (is_array($old_categories) && count($old_categories) > 0) { - foreach($old_categories as $cat_id) + foreach ($old_categories as $cat_id) { - if($cat_id && !$this->categories->check_perms(EGW_ACL_READ, $cat_id)) + if ($cat_id && !$this->categories->check_perms(EGW_ACL_READ, $cat_id)) { $old_cats_preserve[] = $cat_id; } @@ -1112,7 +1271,7 @@ class infolog_bo } $cat_id_list = array(); - foreach($catname_list as $cat_name) + foreach ((array)$catname_list as $cat_name) { $cat_name = trim($cat_name); $cat_id = $this->categories->name2id($cat_name, 'X-'); @@ -1133,7 +1292,7 @@ class infolog_bo } } - if(is_array($old_cats_preserve) && count($old_cats_preserve) > 0) + if (count($old_cats_preserve) > 0) { $cat_id_list = array_merge($old_cats_preserve, $cat_id_list); } @@ -1234,19 +1393,19 @@ class infolog_bo { case 'notify_due_responsible': $info['message'] = lang('%1 you are responsible for is due at %2',$this->enums['type'][$info['info_type']], - $this->tracking->datetime($info['info_enddate']-$this->tz_offset_s,false)); + $this->tracking->datetime($info['info_enddate'],false)); break; case 'notify_due_delegated': $info['message'] = lang('%1 you delegated is due at %2',$this->enums['type'][$info['info_type']], - $this->tracking->datetime($info['info_enddate']-$this->tz_offset_s,false)); + $this->tracking->datetime($info['info_enddate'],false)); break; case 'notify_start_responsible': $info['message'] = lang('%1 you are responsible for is starting at %2',$this->enums['type'][$info['info_type']], - $this->tracking->datetime($info['info_startdate']-$this->tz_offset_s,null)); + $this->tracking->datetime($info['info_startdate'],null)); break; case 'notify_start_delegated': $info['message'] = lang('%1 you delegated is starting at %2',$this->enums['type'][$info['info_type']], - $this->tracking->datetime($info['info_startdate']-$this->tz_offset_s,null)); + $this->tracking->datetime($info['info_startdate'],null)); break; } //error_log("notifiying $user($email) about $info[info_subject]: $info[message]"); @@ -1363,109 +1522,303 @@ class infolog_bo /** * Try to find a matching db entry + * This expects timestamps to be in server-time. * - * @param array $egwData the vTODO data we try to find + * @param array $infoData the infolog data we try to find * @param boolean $relax=false if asked to relax, we only match against some key fields - * @return the infolog_id of the matching entry or false (if none matches) + * @param string $tzid=null timezone, null => user time + * + * @return array of infolog_ids of matching entries */ - function findVTODO($egwData, $relax=false) + function findInfo($infoData, $relax=false, $tzid=null) { - if (!empty($egwData['info_uid'])) - { - $filter = array('col_filter' => array('info_uid' => $egwData['info_uid'])); - if (($found = $this->search($filter)) - && ($uidmatch = array_shift($found))) - { - return $uidmatch['info_id']; - } - } - unset($egwData['info_uid']); - + $foundInfoLogs = array(); $filter = array(); - $description = ''; - if (!empty($egwData['info_des'])) { - $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $egwData['info_des'])); - unset($egwData['info_des']); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '('. ($relax ? 'RELAX, ': 'EXACT, ') . $tzid . ')[InfoData]:' + . array2string($infoData)); + } + + if ($infoData['info_id'] + && ($egwData = $this->read($infoData['info_id'], true, 'server'))) + { + // we only do a simple consistency check + if (!$relax || strpos($egwData['info_subject'], $infoData['info_subject']) === 0) + { + return array($egwData['info_id']); + } + if (!$relax) return array(); + } + unset($infoData['info_id']); + + if (!$relax && !empty($infoData['info_uid'])) + { + $filter = array('col_filter' => array('info_uid' => $infoData['info_uid'])); + foreach($this->so->search($filter) as $egwData) + { + if (!$this->check_access($egwData,EGW_ACL_READ)) continue; + $foundInfoLogs[$egwData['info_id']] = $egwData['info_id']; + } + return $foundInfoLogs; + } + unset($infoData['info_uid']); + + if (empty($infoData['info_des'])) + { + $description = false; + } + else + { + // ignore meta information appendices + $description = trim(preg_replace('/\s*\[[A-Z_]+:.*\].*/im', '', $infoData['info_des'])); + $text = trim(preg_replace('/\s*\[[A-Z_]+:.*\]/im', '', $infoData['info_des'])); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . "()[description]: $description"); + } // Avoid quotation problems - $description = preg_replace("/[^\x20-\x7F].*/", '', $description); - if (strlen($description)) { - $filter['search'] = $description; - } - } - - if ($egwData['info_id'] - && ($found = $this->read($egwData['info_id']))) - { - // We only do a simple consistency check - if ($found['info_subject'] == $egwData['info_subject'] - && strpos($found['info_des'], $description) === 0) + if (preg_match_all('/[\x20-\x7F]*/m', $text, $matches, PREG_SET_ORDER)) { - return $found['info_id']; + $text = ''; + foreach ($matches as $chunk) + { + if (strlen($text) < strlen($chunk[0])) + { + $text = $chunk[0]; + } + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . "()[search]: $text"); + } + $filter['search'] = $text; } } - unset($egwData['info_id']); + $this->time2time($infoData, $tzid, false); + $filter['col_filter'] = $infoData; // priority does not need to match - unset($egwData['info_priority']); + unset($filter['col_filter']['info_priority']); + // we ignore description and location first + unset($filter['col_filter']['info_des']); + unset($filter['col_filter']['info_location']); - $filter['col_filter'] = $egwData; + foreach ($this->so->search($filter) as $itemID => $egwData) + { + if (!$this->check_access($egwData,EGW_ACL_READ)) continue; - if($foundItems = $this->search($filter)) { - if(count($foundItems) > 0) { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; + switch ($infoData['info_type']) + { + case 'task': + if (!empty($egwData['info_location'])) + { + $egwData['info_location'] = str_replace("\r\n", "\n", $egwData['info_location']); + } + if (!$relax && + !empty($infoData['info_location']) && (empty($egwData['info_location']) + || strpos($egwData['info_location'], $infoData['info_location']) !== 0)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[location mismatch]: ' + . $infoData['info_location'] . ' <> ' . $egwData['info_location']); + } + continue; + } + default: + if (!empty($egwData['info_des'])) + { + $egwData['info_des'] = str_replace("\r\n", "\n", $egwData['info_des']); + } + if (!$relax && ($description && empty($egwData['info_des']) + || !empty($egwData['info_des']) && empty($infoData['info_des']) + || strpos($egwData['info_des'], $description) === false)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[description mismatch]: ' + . $infoData['info_des'] . ' <> ' . $egwData['info_des']); + } + continue; + } + // no further criteria to match + $foundInfoLogs[$egwData['info_id']] = $egwData['info_id']; } } - $filter = array(); - - if (!$relax && strlen($description)) { - $filter['search'] = $description; + if (!$relax && !empty($foundInfoLogs)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[FOUND]:' . array2string($foundInfoLogs)); + } + return $foundInfoLogs; } - $filter['col_filter'] = $egwData; + if ($relax) + { + unset($filter['search']); + } - // search for date only match + // search for matches by date only unset($filter['col_filter']['info_startdate']); + unset($filter['col_filter']['info_enddate']); unset($filter['col_filter']['info_datecompleted']); + // Some devices support lesser stati + unset($filter['col_filter']['info_status']); // try tasks without category unset($filter['col_filter']['info_cat']); - #Horde::logMessage("findVTODO Filter\n" - # . print_r($filter, true), - # __FILE__, __LINE__, PEAR_LOG_DEBUG); - foreach ($this->search($filter) as $itemID => $taskData) { - # Horde::logMessage("findVTODO Trying\n" - # . print_r($taskData, true), - # __FILE__, __LINE__, PEAR_LOG_DEBUG); - if (isset($egwData['info_cat']) - && isset($taskData['info_cat']) && $taskData['info_cat'] - && $egwData['info_cat'] != $taskData['info_cat']) continue; - if (isset($egwData['info_startdate']) - && isset($taskData['info_startdate']) && $taskData['info_startdate']) { - $parts = @getdate($taskData['info_startdate']); - $startdate = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); - if ($egwData['info_startdate'] != $startdate) continue; + // Horde::logMessage("findVTODO Filter\n" + // . print_r($filter, true), + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + foreach ($this->so->search($filter) as $itemID => $egwData) + { + if (!$this->check_access($egwData,EGW_ACL_READ)) continue; + // Horde::logMessage("findVTODO Trying\n" + // . print_r($egwData, true), + // __FILE__, __LINE__, PEAR_LOG_DEBUG); + if (isset($infoData['info_cat']) + && isset($egwData['info_cat']) && $egwData['info_cat'] + && $infoData['info_cat'] != $egwData['info_cat']) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[category mismatch]: ' + . $infoData['info_cat'] . ' <> ' . $egwData['info_cat']); + } + continue; } - // some clients don't support DTSTART - if (isset($egwData['info_startdate']) - && (!isset($taskData['info_startdate']) || !$taskData['info_startdate']) - && !$relax) continue; - if (isset($egwData['info_datecompleted']) - && isset($taskData['info_datecompleted']) && $taskData['info_datecompleted']) { - $parts = @getdate($taskData['info_datecompleted']); - $enddate = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); - if ($egwData['info_datecompleted'] != $enddate) continue; + if (isset($infoData['info_startdate']) && $infoData['info_startdate']) + { + // We got a startdate from client + if (isset($egwData['info_startdate']) && $egwData['info_startdate']) + { + // We compare the date only + $taskTime = new egw_time($infoData['info_startdate'],egw_time::$server_timezone); + $egwTime = new egw_time($egwData['info_startdate'],egw_time::$server_timezone); + if ($taskTime->format('Ymd') != $egwTime->format('Ymd')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[start mismatch]: ' + . $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd')); + } + continue; + } + } + elseif (!$relax) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[start mismatch]'); + } + continue; + } } - if ((isset($egwData['info_datecompleted']) - && (!isset($taskData['info_datecompleted']) || !$taskData['info_datecompleted'])) || - (!isset($egwData['info_datecompleted']) - && isset($taskData['info_datecompleted']) && $taskData['info_datecompleted']) - && !$relax) continue; - return($itemID); + if ($infoData['info_type'] == 'task') + { + if (isset($infoData['info_status']) && isset($egwData['info_status']) + && $egwData['info_status'] == 'done' + && $infoData['info_status'] != 'done' || + $egwData['info_status'] != 'done' + && $infoData['info_status'] == 'done') + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[status mismatch]: ' + . $infoData['info_status'] . ' <> ' . $egwData['info_status']); + } + continue; + } + if (isset($infoData['info_enddate']) && $infoData['info_enddate']) + { + // We got a enddate from client + if (isset($egwData['info_enddate']) && $egwData['info_enddate']) + { + // We compare the date only + $taskTime = new egw_time($infoData['info_enddate'],egw_time::$server_timezone); + $egwTime = new egw_time($egwData['info_enddate'],egw_time::$server_timezone); + if ($taskTime->format('Ymd') != $egwTime->format('Ymd')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[DUE mismatch]: ' + . $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd')); + } + continue; + } + } + elseif (!$relax) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[DUE mismatch]'); + } + continue; + } + } + if (isset($infoData['info_datecompleted']) && $infoData['info_datecompleted']) + { + // We got a completed date from client + if (isset($egwData['info_datecompleted']) && $egwData['info_datecompleted']) + { + // We compare the date only + $taskTime = new egw_time($infoData['info_datecompleted'],egw_time::$server_timezone); + $egwTime = new egw_time($egwData['info_datecompleted'],egw_time::$server_timezone); + if ($taskTime->format('Ymd') != $egwTime->format('Ymd')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[completed mismatch]: ' + . $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd')); + } + continue; + } + } + elseif (!$relax) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[completed mismatch]'); + } + continue; + } + } + elseif (!$relax && isset($egwData['info_datecompleted']) && $egwData['info_datecompleted']) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[completed mismatch]'); + } + continue; + } + } + $foundInfoLogs[$itemID] = $itemID; } - return false; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[FOUND]:' . array2string($foundInfoLogs)); + } + return $foundInfoLogs; } } diff --git a/infolog/inc/class.infolog_groupdav.inc.php b/infolog/inc/class.infolog_groupdav.inc.php index 88e27b1c14..fd6f958ea3 100644 --- a/infolog/inc/class.infolog_groupdav.inc.php +++ b/infolog/inc/class.infolog_groupdav.inc.php @@ -29,10 +29,11 @@ class infolog_groupdav extends groupdav_handler * @param string $app 'calendar', 'addressbook' or 'infolog' * @param int $debug=null debug-level to set * @param string $base_uri=null base url of handler + * @param string $principalURL=null pricipal url of handler */ - function __construct($app,$debug=null,$base_uri=null) + function __construct($app,$debug=null,$base_uri=null,$principalURL=null) { - parent::__construct($app,$debug,$base_uri); + parent::__construct($app,$debug,$base_uri,$principalURL); $this->bo = new infolog_bo(); } @@ -56,7 +57,7 @@ class infolog_groupdav extends groupdav_handler if (!is_array($info)) $info = $this->bo->read($info); $name = $info[self::PATH_ATTRIBUTE]; } - return '/infolog/'.$name.'.ics'; + return $name.'.ics'; } /** @@ -72,33 +73,88 @@ class infolog_groupdav extends groupdav_handler { $starttime = microtime(true); + $myself = ($user == $GLOBALS['egw_info']['user']['account_id']); + + if ($options['filters']) + { + + foreach($options['filters'] as $filter) + { + switch($filter['name']) + { + case 'comp-filter': + if ($this->debug > 1) error_log(__METHOD__."($options[path],...) comp-filter='{$filter['attrs']['name']}'"); + + switch($filter['attrs']['name']) + { + case 'VCALENDAR': + continue; + case 'VTODO': + break 3; + default: // We don't handle this + return false; + } + } + } + } + + // check if we have to return the full calendar data or just the etag's + if (!($calendar_data = $options['props'] == 'all' && $options['root']['ns'] == groupdav::CALDAV) && is_array($options['props'])) + { + foreach($options['props'] as $prop) + { + if ($prop['name'] == 'calendar-data') + { + $calendar_data = true; + break; + } + } + } + // todo add a filter to limit how far back entries from the past get synced $filter = array( 'info_type' => 'task', ); + + //if (!$myself) $filter['info_owner'] = $user; + if ($id) $filter['info_id'] = $id; // propfind on a single id // ToDo: add parameter to only return id & etag if (($tasks =& $this->bo->search($params=array( 'order' => 'info_datemodified', 'sort' => 'DESC', - 'filter' => 'own', // filter my: entries user is responsible for, - // filter own: entries the user own or is responsible for + 'filter' => ($myself ? 'own' : 'own'), // filter my: entries user is responsible for, + // filter own: entries the user own or is responsible for + 'date_format' => 'server', 'col_filter' => $filter, )))) { foreach($tasks as &$task) { + $props = array( + HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($task)), + HTTP_WebDAV_Server::mkprop('getcontenttype',$this->agent != 'kde' ? + 'text/calendar; charset=utf-8; component=VTODO' : 'text/calendar'), // Konqueror (3.5) dont understand it otherwise + // getlastmodified and getcontentlength are required by WebDAV and Cadaver eg. reports 404 Not found if not set + HTTP_WebDAV_Server::mkprop('getlastmodified', $task['info_datemodified']), + HTTP_WebDAV_Server::mkprop('resourcetype',''), // DAVKit requires that attribute! + HTTP_WebDAV_Server::mkprop('getcontentlength',''), + ); + if ($calendar_data) + { + $handler = $this->_get_handler(); + $content = $handler->exportVTODO($task,'2.0','PUBLISH'); + $props[] = HTTP_WebDAV_Server::mkprop('getcontentlength',bytes($content)); + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-data',$content); + } + else + { + $props[] = HTTP_WebDAV_Server::mkprop('getcontentlength', ''); // expensive to calculate and no CalDAV client uses it + } $files['files'][] = array( - 'path' => self::get_path($task), - 'props' => array( - HTTP_WebDAV_Server::mkprop('getetag',$this->get_etag($task)), - HTTP_WebDAV_Server::mkprop('getcontenttype',$this->agent != 'kde' ? - 'text/calendar; charset=utf-8; component=VTODO' : 'text/calendar'), // Konqueror (3.5) dont understand it otherwise - // getlastmodified and getcontentlength are required by WebDAV and Cadaver eg. reports 404 Not found if not set - HTTP_WebDAV_Server::mkprop('getlastmodified', $task['info_datemodified']), - HTTP_WebDAV_Server::mkprop('getcontentlength',''), - ), + 'path' => $path.self::get_path($task), + 'props' => $props, ); } } @@ -120,7 +176,7 @@ class infolog_groupdav extends groupdav_handler return $task; } $handler = $this->_get_handler(); - $options['data'] = $handler->exportVTODO($id,'2.0',false,false); // keep UID the client set and no extra charset attributes + $options['data'] = $handler->exportVTODO($id,'2.0','PUBLISH'); $options['mimetype'] = 'text/calendar; charset=utf-8'; header('Content-Encoding: identity'); header('ETag: '.$this->get_etag($task)); @@ -137,22 +193,61 @@ class infolog_groupdav extends groupdav_handler */ function put(&$options,$id,$user=null) { - $ok = $this->_common_get_put_delete('PUT',$options,$id); - if (!is_null($ok) && !is_array($ok)) + if ($this->debug) error_log(__METHOD__."($id, $user)".print_r($options,true)); + + $oldTask = $this->_common_get_put_delete('PUT',$options,$id); + if (!is_null($oldTask) && !is_array($oldTask)) { - return $ok; + return $oldTask; } + $handler = $this->_get_handler(); - if (!($info_id = $handler->importVTODO($options['content'],is_numeric($id) ? $id : -1))) + $vTodo = htmlspecialchars_decode($options['content']); + + if (is_array($oldTask)) + { + $taskId = $oldTask['info_id']; + $retval = true; + } + else + { + // new entry? + if (($foundTasks = $handler->searchVTODO($vTodo))) + { + if (($taskId = array_shift($foundTasks)) && + ($oldTask = $this->bo->read($taskId))) + { + $retval = '301 Moved Permanently'; + } + else + { + // to be safe + $taskId = -1; + $retval = '201 Created'; + } + } + else + { + // new entry + $taskId = -1; + $retval = '201 Created'; + } + } + + if (!($infoId = $handler->importVTODO($vTodo, $taskId, false, $user))) { if ($this->debug) error_log(__METHOD__."(,$id) import_vtodo($options[content]) returned false"); return '403 Forbidden'; } - header('ETag: '.$this->get_etag($info_id)); - if (is_null($ok) || $id != $info_id) + + if ($infoId != $taskId) $retval = '201 Created'; + + header('ETag: '.$this->get_etag($infoId)); + if ($retval !== true) { - header('Location: '.$this->base_uri.self::get_path($info_id)); - return '201 Created'; + $path = preg_replace('|(.*)/[^/]*|', '\1/', $options['path']); + header('Location: '.$this->base_uri.$path.self::get_path($infoId)); + return $retval; } return true; } @@ -181,7 +276,7 @@ class infolog_groupdav extends groupdav_handler */ function read($id) { - return $this->bo->read($id,false); + return $this->bo->read($id,false,'server'); } /** @@ -206,13 +301,45 @@ class infolog_groupdav extends groupdav_handler { if (!is_array($info)) { - $info = $this->bo->read($info); + $info = $this->bo->read($info,true,'server'); } if (!is_array($info) || !isset($info['info_id']) || !isset($info['info_datemodified'])) { return false; } - return '"'.$info['info_id'].':'.$info['info_datemodified'].'"'; + return 'EGw-'.$info['info_id'].':'.$info['info_datemodified'].'-wGE'; + } + + /** + * Add extra properties for calendar collections + * + * @param array $props=array() regular props by the groupdav handler + * @param string $displayname + * @param string $base_uri=null base url of handler + * @return array + */ + static function extra_properties(array $props=array(), $displayname, $base_uri=null) + { + // calendar description + $displayname = $GLOBALS['egw']->translation->convert(lang('Tasks of') . ' ' . + $displayname, + $GLOBALS['egw']->translation->charset(),'utf-8'); + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-description',$displayname); + // email of the current user, see caldav-sheduling draft + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-user-address-set',array( + HTTP_WebDAV_Server::mkprop('href','MAILTO:'.$GLOBALS['egw_info']['user']['email']))); + // supported components, currently only VEVENT + $props[] = HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'supported-calendar-component-set',array( + // HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VEVENT')), + HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'comp',array('name' => 'VTODO')), + )); + + $props[] = HTTP_WebDAV_Server::mkprop('supported-report-set',array( + HTTP_WebDAV_Server::mkprop('supported-report',array( + HTTP_WebDAV_Server::mkprop('report', + HTTP_WebDAV_Server::mkprop(groupdav::CALDAV,'calendar-multiget')))))); + + return $props; } /** diff --git a/infolog/inc/class.infolog_ical.inc.php b/infolog/inc/class.infolog_ical.inc.php index 62bc3bb19e..8ead65250a 100644 --- a/infolog/inc/class.infolog_ical.inc.php +++ b/infolog/inc/class.infolog_ical.inc.php @@ -73,6 +73,13 @@ class infolog_ical extends infolog_bo */ var $uidExtension = false; + /** + * user preference: Use this timezone for import from and export to device + * + * @var string + */ + var $tzid = null; + /** * Client CTCap Properties * @@ -104,14 +111,21 @@ class infolog_ical extends infolog_bo /** * Exports one InfoLog tast to an iCalendar VTODO * - * @param int $_taskID info_id + * @param int|array $task infolog_id or infolog-tasks data * @param string $_version='2.0' could be '1.0' too * @param string $_method='PUBLISH' * @return string/boolean string with vCal or false on error (eg. no permission to read the event) */ - function exportVTODO($_taskID, $_version='2.0',$_method='PUBLISH') + function exportVTODO($task, $_version='2.0',$_method='PUBLISH') { - $taskData = $this->read($_taskID); + if (is_array($task)) + { + $taskData = $task; + } + else + { + if (!($taskData = $this->read($task, true, 'server'))) return false; + } if ($taskData['info_id_parent']) { @@ -144,10 +158,49 @@ class infolog_ical extends infolog_bo $taskData = $GLOBALS['egw']->translation->convert($taskData, $GLOBALS['egw']->translation->charset(), 'UTF-8'); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($taskData)."\n",3,$this->logfile); + } + $vcal = new Horde_iCalendar; $vcal->setAttribute('VERSION',$_version); $vcal->setAttribute('METHOD',$_method); + $tzid = $this->tzid; + + if ($tzid && $tzid != 'UTC') + { + // check if we have vtimezone component data for tzid of event, if not default to user timezone (default to server tz) + if (!($vtimezone = calendar_timezones::tz2id($tzid,'component'))) + { + error_log(__METHOD__."() unknown TZID='$tzid', defaulting to user timezone '".egw_time::$user_timezone->getName()."'!"); + $vtimezone = calendar_timezones::tz2id($tzid=egw_time::$user_timezone->getName(),'component'); + $tzid = null; + } + if (!isset(self::$tz_cache[$tzid])) + { + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + } + // $vtimezone is a string with a single VTIMEZONE component, afaik Horde_iCalendar can not add it directly + // --> we have to parse it and let Horde_iCalendar add it again + $horde_vtimezone = Horde_iCalendar::newComponent('VTIMEZONE',$container=false); + $horde_vtimezone->parsevCalendar($vtimezone,'VTIMEZONE'); + // DTSTART must be in local time! + $standard = $horde_vtimezone->findComponent('STANDARD'); + $dtstart = $standard->getAttribute('DTSTART'); + $dtstart = new egw_time($dtstart, egw_time::$server_timezone); + $dtstart->setTimezone(self::$tz_cache[$tzid]); + $standard->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false); + $daylight = $horde_vtimezone->findComponent('DAYLIGHT'); + $dtstart = $daylight->getAttribute('DTSTART'); + $dtstart = new egw_time($dtstart, egw_time::$server_timezone); + $dtstart->setTimezone(self::$tz_cache[$tzid]); + $daylight->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false); + $vcal->addComponent($horde_vtimezone); + } + $vevent = Horde_iCalendar::newComponent('VTODO',$vcal); if (!isset($this->clientProperties['SUMMARY']['Size'])) @@ -253,15 +306,15 @@ class infolog_ical extends infolog_bo if ($taskData['info_startdate']) { - self::setDateOrTime($vevent,'DTSTART',$taskData['info_startdate']); + self::setDateOrTime($vevent, 'DTSTART', $taskData['info_startdate'], $tzid); } if ($taskData['info_enddate']) { - self::setDateOrTime($vevent,'DUE',$taskData['info_enddate']); + self::setDateOrTime($vevent, 'DUE', $taskData['info_enddate'], false); // export always as date } if ($taskData['info_datecompleted']) { - self::setDateOrTime($vevent,'COMPLETED',$taskData['info_datecompleted']); + self::setDateOrTime($vevent, 'COMPLETED', $taskData['info_datecompleted'], $tzid); } $vevent->setAttribute('DTSTAMP',time()); @@ -298,27 +351,67 @@ class infolog_ical extends infolog_bo } /** - * Check if use set a date or date+time and export it as such + * set date-time attribute to DATE or DATE-TIME depending on value + * 00:00 uses DATE else DATE-TIME * * @param Horde_iCalendar_* $vevent * @param string $attr attribute name - * @param int $value timestamp + * @param int $time timestamp in server-time + * @param string $tzid timezone to use for client, null for user-time, false for server-time */ - static function setDateOrTime($vevent,$attr,$value) + static function setDateOrTime(&$vevent, $attr, $time, $tzid) { - // check if use set only a date --> export it as such - if (date('H:i',$value) == '00:00') + $params = array(); + + if ($tzid) { - $vevent->setAttribute($attr,array( - 'year' => date('Y',$value), - 'month' => date('m',$value), - 'mday' => date('d',$value), - ),array('VALUE' => 'DATE')); + if (!isset(self::$tz_cache[$tzid])) + { + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + } + $tz = self::$tz_cache[$tzid]; + } + elseif(is_null($tzid)) + { + $tz = egw_time::$user_timezone; } else { - $vevent->setAttribute($attr,$value); + $tz = egw_time::$server_timezone; } + if (!is_a($time,'DateTime')) + { + $time = new egw_time($time,egw_time::$server_timezone); + } + $time->setTimezone($tz); + + // check for date --> export it as such + if ($time->format('Hi') == '0000') + { + $arr = egw_time::to($time, 'array'); + $value = array( + 'year' => $arr['year'], + 'month' => $arr['month'], + 'mday' => $arr['day']); + $params['VALUE'] = 'DATE'; + } + else + { + if ($tzid == 'UTC') + { + $value = $time->format('Ymd\THis\Z'); + } + elseif ($tzid) + { + $value = $time->format('Ymd\THis'); + $params['TZID'] = $tzid; + } + else + { + $value = egw_time::to($time, 'ts'); + } + } + $vevent->setAttribute($attr, $value, $params); } /** @@ -327,11 +420,26 @@ class infolog_ical extends infolog_bo * @param string $_vcalData * @param int $_taskID=-1 info_id, default -1 = new entry * @param boolean $merge=false merge data with existing entry + * @param int $user=null delegate new task to this account_id, default null * @return int|boolean integer info_id or false on error */ - function importVTODO(&$_vcalData, $_taskID=-1, $merge=false) + function importVTODO(&$_vcalData, $_taskID=-1, $merge=false, $user=null) { - if (!($taskData = $this->vtodotoegw($_vcalData,$_taskID))) return false; + + if ($this->tzid) + { + date_default_timezone_set($this->tzid); + } + $taskData = $this->vtodotoegw($_vcalData,$_taskID); + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + + if (!$taskData) return false; + + // keep the dates + $this->time2time($taskData, $this->tzid, false); // we suppose that a not set status in a vtodo means that the task did not started yet if (empty($taskData['info_status'])) @@ -344,13 +452,18 @@ class infolog_ical extends infolog_bo $taskData['info_datecompleted'] = 0; } + if (!is_null($user)) + { + $taskData['info_owner'] = $user; + } + if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($taskData)."\n",3,$this->logfile); } - return $this->write($taskData); + return $this->write($taskData, true, true, false); } /** @@ -359,18 +472,29 @@ class infolog_ical extends infolog_bo * @param string $_vcalData VTODO * @param int $contentID=null infolog_id (or null, if unkown) * @param boolean $relax=false if true, a weaker match algorithm is used - * @return infolog_id of a matching entry or false, if nothing was found + * + * @return array of infolog_ids of matching entries */ - function searchVTODO($_vcalData, $contentID=null, $relax=false) { - $result = false; + function searchVTODO($_vcalData, $contentID=null, $relax=false) + { + $result = array(); - if (($egwData = $this->vtodotoegw($_vcalData,$contentID))) + if ($this->tzid) + { + date_default_timezone_set($this->tzid); + } + $taskData = $this->vtodotoegw($_vcalData,$contentID); + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + if ($taskData) { if ($contentID) { - $egwData['info_id'] = $contentID; + $taskData['info_id'] = $contentID; } - $result = $this->findVTODO($egwData, $relax); + $result = $this->findInfo($taskData, $relax, $this->tzid); } return $result; } @@ -386,7 +510,7 @@ class infolog_ical extends infolog_bo { if ($this->log) { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_taskID)\n" . array2string($_vcalData)."\n",3,$this->logfile); } @@ -412,13 +536,10 @@ class infolog_ical extends infolog_bo $minimum_uid_length = 8; } + $taskData = false; + foreach ($vcal->getComponents() as $component) { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . - array2string($component)."\n",3,$this->logfile); - } if (!is_a($component, 'Horde_iCalendar_vtodo')) { if ($this->log) @@ -426,140 +547,135 @@ class infolog_ical extends infolog_bo error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): Not a vTODO container, skipping...\n",3,$this->logfile); } + continue; } - else + + $taskData = array(); + $taskData['info_type'] = 'task'; + + if ($_taskID > 0) { - $taskData = array(); - $taskData['info_type'] = 'task'; - - if ($_taskID > 0) - { - $taskData['info_id'] = $_taskID; - } - foreach ($component->_attributes as $attributes) - { - //$attributes['value'] = trim($attributes['value']); - if (!strlen($attributes['value'])) continue; - - switch ($attributes['name']) - { - case 'CLASS': - $taskData['info_access'] = strtolower($attributes['value']); - break; - - case 'DESCRIPTION': - $value = $attributes['value']; - if (preg_match('/\s*\[UID:(.+)?\]/Usm', $value, $matches)) - { - if (!isset($taskData['info_uid']) - && strlen($matches[1]) >= $minimum_uid_length) - { - $taskData['info_uid'] = $matches[1]; - } - //$value = str_replace($matches[0], '', $value); - } - if (preg_match('/\s*\[PARENT_UID:(.+)?\]/Usm', $value, $matches)) - { - if (!isset($taskData['info_id_parent']) - && strlen($matches[1]) >= $minimum_uid_length) - { - $taskData['info_id_parent'] = $this->getParentID($matches[1]); - } - //$value = str_replace($matches[0], '', $value); - } - $taskData['info_des'] = $value; - break; - - case 'LOCATION': - $taskData['info_location'] = $attributes['value']; - break; - - case 'DUE': - // eGroupWare uses date only - $parts = @getdate($attributes['value']); - $value = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); - $taskData['info_enddate'] = $value; - break; - - case 'COMPLETED': - $taskData['info_datecompleted'] = $attributes['value']; - break; - - case 'DTSTART': - $taskData['info_startdate'] = $attributes['value']; - break; - - case 'PRIORITY': - if (0 <= $attributes['value'] && $attributes['value'] <= 9) { - if ($this->productManufacturer == 'funambol' && - (strpos($this->productName, 'outlook') !== false - || strpos($this->productName, 'pocket pc') !== false)) - { - $taskData['info_priority'] = (int) $this->priority_funambol2egw[$attributes['value']]; - } - else - { - $taskData['info_priority'] = (int) $this->priority_ical2egw[$attributes['value']]; - } - } else { - $taskData['info_priority'] = 1; // default = normal - } - break; - - case 'STATUS': - // check if we (still) have X-INFOLOG-STATUS set AND it would give an unchanged status (no change by the user) - foreach($component->_attributes as $attr) - { - if ($attr['name'] == 'X-INFOLOG-STATUS') break; - } - $taskData['info_status'] = $this->vtodo2status($attributes['value'], - $attr['name'] == 'X-INFOLOG-STATUS' ? $attr['value'] : null); - break; - - case 'SUMMARY': - $taskData['info_subject'] = $attributes['value']; - break; - - case 'RELATED-TO': - $taskData['info_id_parent'] = $this->getParentID($attributes['value']); - break; - - case 'CATEGORIES': - if ($attributes['value']) - { - if($version == '1.0') - { - $vcats = $this->find_or_add_categories(explode(';',$attributes['value']), $_taskID); - } - else - { - $cats = $this->find_or_add_categories(explode(',',$attributes['value']), $_taskID); - } - $taskData['info_cat'] = $cats[0]; - } - break; - - case 'UID': - if (strlen($attributes['value']) >= $minimum_uid_length) { - $taskData['info_uid'] = $attributes['value']; - } - break; - - case 'PERCENT-COMPLETE': - $taskData['info_percent'] = (int) $attributes['value']; - break; - } - } - # the horde ical class does already convert in parsevCalendar - # do NOT convert here - #$taskData = $GLOBALS['egw']->translation->convert($taskData, 'UTF-8'); - - Horde::logMessage("vtodotoegw:\n" . print_r($taskData, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); - - return $taskData; + $taskData['info_id'] = $_taskID; } + foreach ($component->_attributes as $attribute) + { + //$attribute['value'] = trim($attribute['value']); + if (!strlen($attribute['value'])) continue; + + switch ($attribute['name']) + { + case 'CLASS': + $taskData['info_access'] = strtolower($attribute['value']); + break; + + case 'DESCRIPTION': + $value = str_replace("\r\n", "\n", $attribute['value']); + if (preg_match('/\s*\[UID:(.+)?\]/Usm', $value, $matches)) + { + if (!isset($taskData['info_uid']) + && strlen($matches[1]) >= $minimum_uid_length) + { + $taskData['info_uid'] = $matches[1]; + } + //$value = str_replace($matches[0], '', $value); + } + if (preg_match('/\s*\[PARENT_UID:(.+)?\]/Usm', $value, $matches)) + { + if (!isset($taskData['info_id_parent']) + && strlen($matches[1]) >= $minimum_uid_length) + { + $taskData['info_id_parent'] = $this->getParentID($matches[1]); + } + //$value = str_replace($matches[0], '', $value); + } + $taskData['info_des'] = $value; + break; + + case 'LOCATION': + $taskData['info_location'] = str_replace("\r\n", "\n", $attribute['value']); + break; + + case 'DUE': + // eGroupWare uses date only + $parts = @getdate($attribute['value']); + $value = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); + $taskData['info_enddate'] = $value; + break; + + case 'COMPLETED': + $taskData['info_datecompleted'] = $attribute['value']; + break; + + case 'DTSTART': + $taskData['info_startdate'] = $attribute['value']; + break; + + case 'PRIORITY': + if (0 <= $attribute['value'] && $attribute['value'] <= 9) + { + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) + { + $taskData['info_priority'] = (int) $this->priority_funambol2egw[$attribute['value']]; + } + else + { + $taskData['info_priority'] = (int) $this->priority_ical2egw[$attribute['value']]; + } + } + else + { + $taskData['info_priority'] = 1; // default = normal + } + break; + + case 'STATUS': + // check if we (still) have X-INFOLOG-STATUS set AND it would give an unchanged status (no change by the user) + foreach ($component->_attributes as $attr) + { + if ($attr['name'] == 'X-INFOLOG-STATUS') break; + } + $taskData['info_status'] = $this->vtodo2status($attribute['value'], + $attr['name'] == 'X-INFOLOG-STATUS' ? $attr['value'] : null); + break; + + case 'SUMMARY': + $taskData['info_subject'] = str_replace("\r\n", "\n", $attribute['value']); + break; + + case 'RELATED-TO': + $taskData['info_id_parent'] = $this->getParentID($attribute['value']); + break; + + case 'CATEGORIES': + if (!empty($attribute['value'])) + { + $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_taskID); + $taskData['info_cat'] = $cats[0]; + } + break; + + case 'UID': + if (strlen($attribute['value']) >= $minimum_uid_length) + { + $taskData['info_uid'] = $attribute['value']; + } + break; + + case 'PERCENT-COMPLETE': + $taskData['info_percent'] = (int) $attribute['value']; + break; + } + } + break; } - return false; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_taskID)\n" . + ($taskData ? array2string($taskData) : 'FALSE') . "\n",3,$this->logfile); + } + return $taskData; } /** @@ -571,7 +687,8 @@ class infolog_ical extends infolog_bo */ function exportVNOTE($_noteID, $_type) { - $note = $this->read($_noteID); + if(!($note = $this->read($_noteID, true, 'server'))) return false; + $note = $GLOBALS['egw']->translation->convert($note, $GLOBALS['egw']->translation->charset(), 'UTF-8'); @@ -580,7 +697,6 @@ class infolog_ical extends infolog_bo case 'text/plain': $txt = $note['info_subject']."\n\n".$note['info_des']; return $txt; - break; case 'text/x-vnote': if (!empty($note['info_cat'])) @@ -631,7 +747,10 @@ class infolog_ical extends infolog_bo { $vnote->setAttribute('DCREATED',$note['info_startdate']); } - $vnote->setAttribute('DCREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'add')); + else + { + $vnote->setAttribute('DCREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'add')); + } $vnote->setAttribute('LAST-MODIFIED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'modify')); #$vnote->setAttribute('CLASS',$taskData['info_access'] == 'public' ? 'PUBLIC' : 'PRIVATE'); @@ -676,7 +795,7 @@ class infolog_ical extends infolog_bo array2string($note)."\n",3,$this->logfile); } - return $this->write($note); + return $this->write($note, true, true, false); } /** @@ -684,40 +803,19 @@ class infolog_ical extends infolog_bo * * @param string $_vcalData VNOTE * @param int $contentID=null infolog_id (or null, if unkown) + * @param boolean $relax=false if true, a weaker match algorithm is used + * * @return infolog_id of a matching entry or false, if nothing was found */ - function searchVNOTE($_vcalData, $_type, $contentID=null) + function searchVNOTE($_vcalData, $_type, $contentID=null, $relax=false) { - if (!($note = $this->vnotetoegw($_vcalData,$_type,$contentID))) return false; + if (!($note = $this->vnotetoegw($_vcalData,$_type,$contentID))) return array(); if ($contentID) $note['info_id'] = $contentID; unset($note['info_startdate']); - $filter = array(); - - if (!empty($note['info_des'])) - { - $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $note['info_des'])); - unset($note['info_des']); - if (strlen($description)) - { - $filter['search'] = $description; - } - } - - $filter['col_filter'] = $note; - - if (($foundItems = $this->search($filter))) - { - if (count($foundItems) > 0) - { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; - } - } - - return false; + return $this->findInfo($note, $relax, $this->tzid); } /** @@ -730,6 +828,13 @@ class infolog_ical extends infolog_bo */ function vnotetoegw($_data, $_type, $_noteID=-1) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_type, $_noteID)\n" . + array2string($_data)."\n",3,$this->logfile); + } + $note = false; + switch ($_type) { case 'text/plain': @@ -739,19 +844,15 @@ class infolog_ical extends infolog_bo $txt = $botranslation->convert($_data, 'utf-8'); $txt = str_replace("\r\n", "\n", $txt); - if (preg_match("/^(^\n)\n\n(.*)$/", $txt, $match)) + if (preg_match('/([^\n]+)\n\n(.*)/m', $txt, $match)) { - $note['info_subject'] = $match[0]; - $note['info_des'] = $match[1]; + $note['info_subject'] = $match[1]; + $note['info_des'] = $match[2]; } else { - // should better be imported as subject, but causes duplicates - // TODO: should be examined - $note['info_des'] = $txt; + $note['info_subject'] = $txt; } - - return $note; break; case 'text/x-vnote': @@ -772,34 +873,31 @@ class infolog_ical extends infolog_bo switch ($attribute['name']) { case 'BODY': - $note['info_des'] = $attribute['value']; + $note['info_des'] = str_replace("\r\n", "\n", $attribute['value']); break; case 'SUMMARY': - $note['info_subject'] = $attribute['value']; + $note['info_subject'] = str_replace("\r\n", "\n", $attribute['value']); break; case 'CATEGORIES': if ($attribute['value']) { - if($version == '1.0') - { - $cats = $this->find_or_add_categories(explode(';',$attribute['value']), $_noteID); - } - else - { - $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_noteID); - } + $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_noteID); $note['info_cat'] = $cats[0]; } break; } } } - return $note; } } - return false; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_type, $_noteID)\n" . + ($note ? array2string($note) : 'FALSE') ."\n",3,$this->logfile); + } + return $note; } /** @@ -842,9 +940,36 @@ class infolog_ical extends infolog_bo { $this->uidExtension = true; } + if (isset($deviceInfo['tzid']) && + $deviceInfo['tzid']) + { + switch ($deviceInfo['tzid']) + { + case 1: + $this->tzid = false; + break; + case 2: + $this->tzid = null; + break; + default: + $this->tzid = $deviceInfo['tzid']; + } + } } - Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $this->productManufacturer . + ', '. $this->productName .', ' . + ($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) . + ")\n" , 3, $this->logfile); + } + + Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' + . $this->productName .', ' . + ($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) .')', + __FILE__, __LINE__, PEAR_LOG_DEBUG); } } diff --git a/infolog/inc/class.infolog_sif.inc.php b/infolog/inc/class.infolog_sif.inc.php index 25a5d1d149..b1bc29a28b 100644 --- a/infolog/inc/class.infolog_sif.inc.php +++ b/infolog/inc/class.infolog_sif.inc.php @@ -90,6 +90,67 @@ class infolog_sif extends infolog_bo */ var $uidExtension = false; + /** + * user preference: Use this timezone for import from and export to device + * + * @var string + */ + var $tzid = null; + + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + var $logfile="/tmp/log-infolog-sif"; + + /** + * Constructor + * + */ + function __construct() + { + parent::__construct(); + if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-infolog-sif"; + $this->vCalendar = new Horde_iCalendar; + } + + /** + * Get DateTime value for a given time and timezone + * + * @param int|string|DateTime $time in server-time as returned by calendar_bo for $data_format='server' + * @param string $tzid TZID of event or 'UTC' or NULL for palmos timestamps in usertime + * + */ + function getDateTime($time, $tzid) + { + if (empty($tzid) || $tzid == 'UTC') + { + return $this->vCalendar->_exportDateTime(egw_time::to($time,'ts')); + } + if (!is_a($time,'DateTime')) + { + $time = new egw_time($time,egw_time::$server_timezone); + } + if (!isset(self::$tz_cache[$tzid])) + { + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + } + // check for date --> export it as such + if ($time->format('Hi') == '0000') + { + $arr = egw_time::to($time, 'array'); + $time = new egw_time($arr, self::$tz_cache[$tzid]); + $value = $time->format('Y-m-d'); + } + else + { + $time->setTimezone(self::$tz_cache[$tzid]); + $value = $time->format('Ymd\THis'); + } + return $value; + } function startElement($_parser, $_tag, $_attributes) { @@ -121,14 +182,15 @@ class infolog_sif extends infolog_bo */ function siftoegw($sifData, $_sifType, $_id=-1) { + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_sifType, $_id)\n" . + array2string($sifData) . "\n", 3, $this->logfile); + } + $sysCharSet = $GLOBALS['egw']->translation->charset(); - #$tmpfname = tempnam('/tmp/sync/contents','sift_'); - - #$handle = fopen($tmpfname, "w"); - #fwrite($handle, $sifData); - #fclose($handle); - switch ($_sifType) { case 'note': @@ -136,9 +198,12 @@ class infolog_sif extends infolog_bo break; case 'task': - default: $this->_currentSIFMapping = $this->_sifTaskMapping; break; + + default: + // we don't know how to handle this + return false; } $this->xml_parser = xml_parser_create('UTF-8'); @@ -156,16 +221,22 @@ class infolog_sif extends infolog_bo return false; } - if (!array($this->_extractedSIFData)) return false; + if (!array($this->_extractedSIFData)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[PARSER FAILD]\n", + 3, $this->logfile); + } + return false; + } + $infoData = array(); switch ($_sifType) { case 'task': - $taskData = array(); - $vcal = new Horde_iCalendar; - - $taskData['info_type'] = 'task'; - $taskData['info_status'] = 'not-started'; + $infoData['info_type'] = 'task'; + $infoData['info_status'] = 'not-started'; if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { @@ -186,7 +257,7 @@ class infolog_sif extends infolog_bo switch($key) { case 'info_access': - $taskData[$key] = ((int)$value > 0) ? 'private' : 'public'; + $infoData[$key] = ((int)$value > 0) ? 'private' : 'public'; break; case 'info_datecompleted': @@ -194,9 +265,9 @@ class infolog_sif extends infolog_bo case 'info_startdate': if (!empty($value)) { - $taskData[$key] = $vcal->_parseDateTime($value); + $infoData[$key] = $this->vCalendar->_parseDateTime($value); // somehow the client always deliver a timestamp about 3538 seconds, when no startdate set. - if ($taskData[$key] < 10000) unset($taskData[$key]); + if ($infoData[$key] < 10000) unset($infoData[$key]); } break; @@ -205,49 +276,48 @@ class infolog_sif extends infolog_bo if (!empty($value)) { $categories = $this->find_or_add_categories(explode(';', $value), $_id); - $taskData['info_cat'] = $categories[0]; + $infoData['info_cat'] = $categories[0]; } break; case 'info_priority': - $taskData[$key] = (int)$value; + $infoData[$key] = (int)$value; break; case 'info_status': switch ($value) { case '0': - $taskData[$key] = 'not-started'; + $infoData[$key] = 'not-started'; break; case '1': - $taskData[$key] = 'ongoing'; + $infoData[$key] = 'ongoing'; break; case '2': - $taskData[$key] = 'done'; - $taskData['info_percent'] = 100; + $infoData[$key] = 'done'; + $infoData['info_percent'] = 100; break; case '3': - $taskData[$key] = 'waiting'; + $infoData[$key] = 'waiting'; break; case '4': if ($this->productName == 'blackberry plug-in') { - $taskData[$key] = 'deferred'; + $infoData[$key] = 'deferred'; } else { - $taskData[$key] = 'cancelled'; + $infoData[$key] = 'cancelled'; } break; default: - $taskData[$key] = 'ongoing'; - break; + $infoData[$key] = 'ongoing'; } break; case 'complete': - $taskData['info_status'] = 'done'; - $taskData['info_percent'] = 100; + $infoData['info_status'] = 'done'; + $infoData['info_percent'] = 100; break; case 'info_des': @@ -256,7 +326,7 @@ class infolog_sif extends infolog_bo { if (strlen($matches[1]) >= $minimum_uid_length) { - $taskData['info_uid'] = $matches[1]; + $infoData['info_uid'] = $matches[1]; } //$value = str_replace($matches[0], '', $value); } @@ -264,25 +334,28 @@ class infolog_sif extends infolog_bo { if (strlen($matches[1]) >= $minimum_uid_length) { - $taskData['info_id_parent'] = $this->getParentID($matches[1]); + $infoData['info_id_parent'] = $this->getParentID($matches[1]); } //$value = str_replace($matches[0], '', $value); } default: - $taskData[$key] = $value; - break; + $infoData[$key] = str_replace("\r\n", "\n", $value); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + "key=$key => value=" . $infoData[$key] . "\n", 3, $this->logfile); } - #error_log("infolog task key=$key => value=" . $taskData[$key]); } - - return $taskData; + if (empty($infoData['info_datecompleted'])) + { + $infoData['info_datecompleted'] = 0; + } break; case 'note': - $noteData = array(); - $noteData['info_type'] = 'note'; - $vcal = new Horde_iCalendar; + $infoData['info_type'] = 'note'; foreach ($this->_extractedSIFData as $key => $value) { @@ -295,13 +368,13 @@ class infolog_sif extends infolog_bo case 'info_startdate': if (!empty($value)) { - $noteData[$key] = $vcal->_parseDateTime($value); + $infoData[$key] = $this->vCalendar->_parseDateTime($value); // somehow the client always deliver a timestamp about 3538 seconds, when no startdate set. - if ($noteData[$key] < 10000) $noteData[$key] = ''; + if ($infoData[$key] < 10000) $infoData[$key] = ''; } else { - $noteData[$key] = ''; + $infoData[$key] = ''; } break; @@ -309,23 +382,26 @@ class infolog_sif extends infolog_bo if (!empty($value)) { $categories = $this->find_or_add_categories(explode(';', $value), $_id); - $noteData['info_cat'] = $categories[0]; + $infoData['info_cat'] = $categories[0]; } break; default: - $noteData[$key] = $value; - break; + $infoData[$key] = str_replace("\r\n", "\n", $value); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + "key=$key => value=" . $infoData[$key] . "\n", 3, $this->logfile); } - #error_log("infolog note key=$key => value=".$noteData[$key]); } - return $noteData; - break; - - - default: - return false; } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($infoData) . "\n", 3, $this->logfile); + } + return $infoData; } /** @@ -339,28 +415,13 @@ class infolog_sif extends infolog_bo */ function searchSIF($_sifData, $_sifType, $contentID=null, $relax=false) { - if (!($egwData = $this->siftoegw($_sifData, $_sifType, $contentID))) return false; + if (!($egwData = $this->siftoegw($_sifData, $_sifType, $contentID))) return array(); if ($contentID) $egwData['info_id'] = $contentID; - if ($_sifType == 'task') return $this->findVTODO($egwData, $relax); - if ($_sifType == 'note') unset($egwData['info_startdate']); - $filter = array(); - - $filter['col_filter'] = $egwData; - - if ($foundItems = $this->search($filter)) - { - if (count($foundItems) > 0) - { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; - } - } - - return false; + return $this->findInfo($egwData, $relax, $this->tzid); } /** @@ -373,18 +434,20 @@ class infolog_sif extends infolog_bo */ function addSIF($_sifData, $_id, $_sifType, $merge=false) { - if (!($egwData = $this->siftoegw($_sifData, $_sifType, $_id))) return false; + if ($this->tzid) + { + date_default_timezone_set($this->tzid); + } + $egwData = $this->siftoegw($_sifData, $_sifType, $_id); + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + if (!$egwData) return false; if ($_id > 0) $egwData['info_id'] = $_id; - if (empty($taskData['info_datecompleted'])) - { - $taskData['info_datecompleted'] = 0; - } - - $egwID = $this->write($egwData, false); - - return $egwID; + return $this->write($egwData, true, true, false); } @@ -399,189 +462,161 @@ class infolog_sif extends infolog_bo { $sysCharSet = $GLOBALS['egw']->translation->charset(); + if (!($infoData = $this->read($_id, true, 'server'))) return false; + switch($_sifType) { case 'task': - if (($taskData = $this->read($_id))) + if ($infoData['info_id_parent']) { - $vcal = new Horde_iCalendar('1.0'); - - if ($taskData['info_id_parent']) - { - $parent = $this->read($taskData['info_id_parent']); - $taskData['info_id_parent'] = $parent['info_uid']; - } - else - { - $taskData['info_id_parent'] = ''; - } - - if (!preg_match('/\[UID:.+\]/m', $taskData['info_des'])) - { - $taskData['info_des'] .= "\r\n[UID:" . $taskData['info_uid'] . "]"; - if ($taskData['info_id_parent'] != '') - { - $taskData['info_des'] .= "\r\n[PARENT_UID:" . $taskData['info_id_parent'] . "]"; - } - } - - $sifTask = self::xml_decl . "\n" . self::SIF_decl; - - foreach ($this->_sifTaskMapping as $sifField => $egwField) - { - if (empty($egwField)) continue; - - $value = $GLOBALS['egw']->translation->convert($taskData[$egwField], $sysCharSet, 'utf-8'); - - switch ($sifField) - { - - case 'Complete': - // is handled with DateCompleted - break; - - case 'DateCompleted': - if ($taskData[info_status] == 'done') - { - $sifTask .= "1"; - } - else - { - $sifTask .= "0"; - continue; - } - case 'DueDate': - if (!empty($value)) - { - $hdate = new Horde_Date($value); - $value = $vcal->_exportDate($hdate, '000000Z'); - $sifTask .= "<$sifField>$value"; - } - else - { - $sifTask .= "<$sifField>"; - } - break; - case 'StartDate': - if (!empty($value)) - { - $value = $vcal->_exportDateTime($value); - $sifTask .= "<$sifField>$value"; - } - else - { - $sifTask .= "<$sifField>"; - } - break; - - case 'Importance': - if ($value > 3) $value = 3; - $sifTask .= "<$sifField>$value"; - break; - - case 'Sensitivity': - $value = ($value == 'private' ? '2' : '0'); - $sifTask .= "<$sifField>$value"; - break; - - case 'Status': - switch ($value) - { - case 'cancelled': - case 'deferred': - $value = '4'; - break; - case 'waiting': - case 'nonactive': - $value = '3'; - break; - case 'done': - case 'archive': - case 'billed': - $value = '2'; - break; - case 'not-started': - case 'template': - $value = '0'; - break; - default: //ongoing - $value = 1; - break; - } - $sifTask .= "<$sifField>$value"; - break; - - case 'Categories': - if (!empty($value) && $value) - { - $value = implode('; ', $this->get_categories(array($value))); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - else - { - break; - } - - default: - $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); - $sifTask .= "<$sifField>$value"; - break; - } - } - $sifTask .= '00'; - return $sifTask; + $parent = $this->read($infoData['info_id_parent']); + $infoData['info_id_parent'] = $parent['info_uid']; } - break; + else + { + $infoData['info_id_parent'] = ''; + } + + if (!preg_match('/\[UID:.+\]/m', $infoData['info_des'])) + { + $infoData['info_des'] .= "\r\n[UID:" . $infoData['info_uid'] . "]"; + if ($infoData['info_id_parent'] != '') + { + $infoData['info_des'] .= "\r\n[PARENT_UID:" . $infoData['info_id_parent'] . "]"; + } + } + + $sifTask = self::xml_decl . "\n" . self::SIF_decl; + + foreach ($this->_sifTaskMapping as $sifField => $egwField) + { + if (empty($egwField)) continue; + + $value = $GLOBALS['egw']->translation->convert($infoData[$egwField], $sysCharSet, 'utf-8'); + + switch ($sifField) + { + + case 'Complete': + // is handled with DateCompleted + break; + + case 'DateCompleted': + if ($infoData[info_status] != 'done') + { + $sifTask .= "0"; + continue; + } + $sifTask .= "1"; + + case 'DueDate': + case 'StartDate': + $sifTask .= "<$sifField>"; + if (!empty($value)) + { + $sifTask .= $this->getDateTime($value, $this->tzid); + } + $sifTask .= ""; + break; + + case 'Importance': + if ($value > 3) $value = 3; + $sifTask .= "<$sifField>$value"; + break; + + case 'Sensitivity': + $value = ($value == 'private' ? '2' : '0'); + $sifTask .= "<$sifField>$value"; + break; + + case 'Status': + switch ($value) + { + case 'cancelled': + case 'deferred': + $value = '4'; + break; + case 'waiting': + case 'nonactive': + $value = '3'; + break; + case 'done': + case 'archive': + case 'billed': + $value = '2'; + break; + case 'not-started': + case 'template': + $value = '0'; + break; + default: //ongoing + $value = 1; + break; + } + $sifTask .= "<$sifField>$value"; + break; + + case 'Categories': + if (!empty($value) && $value) + { + $value = implode('; ', $this->get_categories(array($value))); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } + else + { + break; + } + + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); + $sifTask .= "<$sifField>$value"; + break; + } + } + $sifTask .= '00'; + return $sifTask; case 'note': - if (($taskData = $this->read($_id))) + $sifNote = self::xml_decl . "\n" . self::SIF_decl; + + foreach ($this->_sifNoteMapping as $sifField => $egwField) { - $vcal = new Horde_iCalendar('1.0'); + if(empty($egwField)) continue; - $sifNote = self::xml_decl . "\n" . self::SIF_decl; + $value = $GLOBALS['egw']->translation->convert($infoData[$egwField], $sysCharSet, 'utf-8'); - foreach ($this->_sifNoteMapping as $sifField => $egwField) + switch ($sifField) { - if(empty($egwField)) continue; + case 'Date': + $sifNote .= '<$sifField>'; + if (!empty($value)) + { + $sifNote .= $this->getDateTime($value, $this->tzid); + } + $sifNote .= ''; + break; - $value = $GLOBALS['egw']->translation->convert($taskData[$egwField], $sysCharSet, 'utf-8'); - - switch ($sifField) - { - case 'Date': - if (!empty($value)) - { - $value = $vcal->_exportDateTime($value); - } - $sifNote .= "<$sifField>$value"; + case 'Categories': + if (!empty($value)) + { + $value = implode('; ', $this->get_categories(array($value))); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } + else + { break; + } - case 'Categories': - if (!empty($value)) - { - $value = implode('; ', $this->get_categories(array($value))); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - else - { - break; - } - - default: - $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); - $sifNote .= "<$sifField>$value"; - break; - } + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); + $sifNote .= "<$sifField>$value"; + break; } - $sifNote .= ''; - return $sifNote; } - break; - - default; - return false; + $sifNote .= ''; + return $sifNote; } - + return false; } /** @@ -604,6 +639,21 @@ class infolog_sif extends infolog_bo { $this->uidExtension = true; } + if (isset($deviceInfo['tzid']) && + $deviceInfo['tzid']) + { + switch ($deviceInfo['tzid']) + { + case 1: + $this->tzid = false; + break; + case 2: + $this->tzid = null; + break; + default: + $this->tzid = $deviceInfo['tzid']; + } + } } // store product name and version, to be able to use it elsewhere if ($_productName) diff --git a/infolog/inc/class.infolog_so.inc.php b/infolog/inc/class.infolog_so.inc.php index 4cc6153469..95684c8e20 100644 --- a/infolog/inc/class.infolog_so.inc.php +++ b/infolog/inc/class.infolog_so.inc.php @@ -139,19 +139,27 @@ class infolog_so /** * Filter for a given responsible user: info_responsible either contains a the user or one of his memberships * - * @param int $user + * @param int|array $users one or more account_ids * @return string * * @todo make the responsible a second table and that filter a join with the responsible table */ - function responsible_filter($user) + function responsible_filter($users) { - if (!$user) return '0'; + if (!$users) return '0'; - $responsible = $user > 0 ? $GLOBALS['egw']->accounts->memberships($user,true) : - $GLOBALS['egw']->accounts->members($user,true); - - $responsible[] = $user; + $responsible = array(); + foreach((array)$users as $user) + { + $responsible = array_merge($responsible, + $user > 0 ? $GLOBALS['egw']->accounts->memberships($user,true) : + $GLOBALS['egw']->accounts->members($user,true)); + $responsible[] = $user; + } + if (is_array($users)) + { + $responsible = array_unique($responsible); + } foreach($responsible as $key => $uid) { $responsible[$key] = $this->db->concat("','",'info_responsible',"','")." LIKE '%,$uid,%'"; @@ -171,14 +179,18 @@ class infolog_so */ function aclFilter($filter = False) { - preg_match('/(my|responsible|delegated|own|privat|private|all|none|user)([0-9]*)/',$filter_was=$filter,$vars); + preg_match('/(my|responsible|delegated|own|privat|private|all|none|user)([0-9,-]*)/',$filter_was=$filter,$vars); $filter = $vars[1]; - $f_user = intval($vars[2]); + $f_user = $vars[2]; if (isset($this->acl_filter[$filter.$f_user])) { return $this->acl_filter[$filter.$f_user]; // used cached filter if found } + if ($f_user && strpos($f_user,',') !== false) + { + $f_user = explode(',',$f_user); + } $filtermethod = " (info_owner=$this->user"; // user has all rights @@ -236,9 +248,11 @@ class infolog_so } $filtermethod .= ') '; - if ($filter == 'user' && $f_user > 0) + if ($filter == 'user' && $f_user) { - $filtermethod .= " AND (info_owner=$f_user AND info_responsible='0' OR ".$this->responsible_filter($f_user).')'; + $filtermethod .= $this->db->expression($this->info_table,' AND (',array( + 'info_owner' => $f_user, + )," AND info_responsible='0' OR ",$this->responsible_filter($f_user),')'); } } //echo "

aclFilter(filter='$filter_was',user='$user') = '$filtermethod', privat_user_list=".print_r($privat_user_list,True).", public_user_list=".print_r($public_user_list,True)."

\n"; diff --git a/infolog/inc/class.infolog_tracking.inc.php b/infolog/inc/class.infolog_tracking.inc.php index ad7fd48fa4..09821b3507 100644 --- a/infolog/inc/class.infolog_tracking.inc.php +++ b/infolog/inc/class.infolog_tracking.inc.php @@ -163,17 +163,17 @@ class infolog_tracking extends bo_tracking if (!$old || $old['info_status'] == 'deleted') { return lang('New %1 created by %2 at %3',lang($this->infolog->enums['type'][$data['info_type']]), - $GLOBALS['egw']->common->grab_owner_name($this->infolog->user),$this->datetime(time())); + common::grab_owner_name($this->infolog->user),$this->datetime('now')); } elseif($data['info_status'] == 'deleted') { return lang('%1 deleted by %2 at %3',lang($this->infolog->enums['type'][$data['info_type']]), - $GLOBALS['egw']->common->grab_owner_name($data['info_modifier']), - $this->datetime($data['info_datemodified']-$this->infolog->tz_offset_s)); + common::grab_owner_name($data['info_modifier']), + $this->datetime($data['info_datemodified'])); } return lang('%1 modified by %2 at %3',lang($this->infolog->enums['type'][$data['info_type']]), - $GLOBALS['egw']->common->grab_owner_name($data['info_modifier']), - $this->datetime($data['info_datemodified']-$this->infolog->tz_offset_s)); + common::grab_owner_name($data['info_modifier']), + $this->datetime($data['info_datemodified'])); } /** @@ -192,7 +192,7 @@ class infolog_tracking extends bo_tracking { foreach($data['info_responsible'] as $uid) { - $responsible[] = $GLOBALS['egw']->common->grab_owner_name($uid); + $responsible[] = common::grab_owner_name($uid); } } if ($GLOBALS['egw_info']['user']['preferences']['infolog']['show_id']) @@ -205,13 +205,13 @@ class infolog_tracking extends bo_tracking 'info_addr' => $data['info_addr'], 'info_cat' => $data['info_cat'] ? $GLOBALS['egw']->categories->id2name($data['info_cat']) : '', 'info_priority' => lang($this->infolog->enums['priority'][$data['info_priority']]), - 'info_owner' => $GLOBALS['egw']->common->grab_owner_name($data['info_owner']), + 'info_owner' => common::grab_owner_name($data['info_owner']), 'info_status' => lang($data['info_status']=='deleted'?'deleted':$this->infolog->status[$data['info_type']][$data['info_status']]), 'info_percent' => (int)$data['info_percent'].'%', - 'info_datecompleted' => $data['info_datecompleted'] ? $this->datetime($data['info_datecompleted']-$this->infolog->tz_offset_s) : '', + 'info_datecompleted' => $data['info_datecompleted'] ? $this->datetime($data['info_datecompleted']) : '', 'info_location' => $data['info_location'], - 'info_startdate' => $data['info_startdate'] ? $this->datetime($data['info_startdate']-$this->infolog->tz_offset_s,null) : '', - 'info_enddate' => $data['info_enddate'] ? $this->datetime($data['info_enddate']-$this->infolog->tz_offset_s,false) : '', + 'info_startdate' => $data['info_startdate'] ? $this->datetime($data['info_startdate'],null) : '', + 'info_enddate' => $data['info_enddate'] ? $this->datetime($data['info_enddate'],false) : '', 'info_responsible' => implode(', ',$responsible), 'info_subject' => $data['info_subject'], ) as $name => $value)