diff --git a/infolog/inc/class.infolog_bo.inc.php b/infolog/inc/class.infolog_bo.inc.php index f80cf427ec..ed152dc31a 100644 --- a/infolog/inc/class.infolog_bo.inc.php +++ b/infolog/inc/class.infolog_bo.inc.php @@ -4,6 +4,7 @@ * * @link http://www.egroupware.org * @author Ralf Becker + * @author Joerg Lehrke * @package infolog * @copyright (c) 2003-8 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -154,7 +155,7 @@ class infolog_bo 'billed' => 'billed', // --> DONE 'template' => 'template', // --> cancelled 'nonactive' => 'nonactive', // --> cancelled - 'archive' => 'archive' ), // --> cancelled + 'archive' => 'archive' ), // --> cancelled 'phone' => array( 'not-started' => 'call', // iCal NEEDS-ACTION 'ongoing' => 'will-call', // iCal IN-PROCESS @@ -513,7 +514,6 @@ class infolog_bo $GLOBALS['egw']->contenthistory->updateTimeStamp('infolog_'.$info['info_type'], $info_id, 'delete', time()); // send email notifications and do the history logging - require_once(EGW_INCLUDE_ROOT.'/infolog/inc/class.infolog_tracking.inc.php'); if (!is_object($this->tracking)) { $this->tracking = new infolog_tracking($this); @@ -533,7 +533,7 @@ class infolog_bo * @param boolean $touch_modified=true touch the modification data and sets the modiefier's user-id * @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) { //echo "boinfolog::write()values="; _debug_array($values); if ($status_only = $values['info_id'] && !$this->check_access($values['info_id'],EGW_ACL_EDIT)) @@ -578,7 +578,7 @@ class infolog_bo if ($set_completed) { $values['info_datecompleted'] = $this->user_time_now; - $values['info_percent'] = '100%'; + $values['info_percent'] = 100; $forcestatus = true; $status = 'done'; if (isset($values['info_type']) && !in_array($values['info_status'],array('done','billed','cancelled'))) { @@ -608,7 +608,7 @@ class infolog_bo } if (in_array($values['info_status'],array('done','billed'))) { - $values['info_percent'] = '100'; + $values['info_percent'] = 100; } if ((int)$values['info_percent'] == 100 && !in_array($values['info_status'],array('done','billed','cancelled'))) { @@ -659,7 +659,7 @@ class infolog_bo // 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! - $xmprpc = is_object($GLOBALS['server']) && $GLOBALS['server']->last_method; + $xmlrpc = is_object($GLOBALS['server']) && $GLOBALS['server']->last_method; $check_modified = $values['info_datemodified'] && !$xmlrpc ? $values['info_datemodified']-$this->tz_offset_s : false; $values['info_datemodified'] = $this->user_time_now; } @@ -1080,7 +1080,7 @@ class infolog_bo $cat_id = $this->categories->name2id($cat_name, 'X-'); if (!$cat_id) { - $cat_id = $this->categories->add(array('name' => $cat_name,'descr' => $cat_name)); + $cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private')); } if ($cat_id) @@ -1172,32 +1172,32 @@ class infolog_bo // check if we already send a notification for that infolog entry, eg. starting and due on same day if (in_array($info['info_id'],$notified_info_ids)) continue; - if (is_null($tracking) || $tracking->user != $user) + if (is_null($this->tracking) || $this->tracking->user != $user) { require_once(EGW_INCLUDE_ROOT.'/infolog/inc/class.infolog_tracking.inc.php'); - $tracking = new infolog_tracking($this); + $this->tracking = new infolog_tracking($this); } switch($pref) { case 'notify_due_responsible': $info['message'] = lang('%1 you are responsible for is due at %2',$this->enums['type'][$info['info_type']], - $tracking->datetime($info['info_enddate']-$this->tz_offset_s,false)); + $this->tracking->datetime($info['info_enddate']-$this->tz_offset_s,false)); break; case 'notify_due_delegated': $info['message'] = lang('%1 you delegated is due at %2',$this->enums['type'][$info['info_type']], - $tracking->datetime($info['info_enddate']-$this->tz_offset_s,false)); + $this->tracking->datetime($info['info_enddate']-$this->tz_offset_s,false)); break; case 'notify_start_responsible': $info['message'] = lang('%1 you are responsible for is starting at %2',$this->enums['type'][$info['info_type']], - $tracking->datetime($info['info_startdate']-$this->tz_offset_s,null)); + $this->tracking->datetime($info['info_startdate']-$this->tz_offset_s,null)); break; case 'notify_start_delegated': $info['message'] = lang('%1 you delegated is starting at %2',$this->enums['type'][$info['info_type']], - $tracking->datetime($info['info_startdate']-$this->tz_offset_s,null)); + $this->tracking->datetime($info['info_startdate']-$this->tz_offset_s,null)); break; } error_log("notifiying $user($email) about $info[info_subject]: $info[message]"); - $tracking->send_notification($info,null,$email,$user,$pref); + $this->tracking->send_notification($info,null,$email,$user,$pref); $notified_info_ids[] = $info['info_id']; } @@ -1221,6 +1221,8 @@ class infolog_bo 'template' => 'CANCELLED', 'nonactive' => 'CANCELLED', 'archive' => 'CANCELLED', + 'deferred' => 'NEEDS-ACTION', + 'waiting' => 'IN-PROCESS', ); /** conversion of vtodo status to infolog status @@ -1229,7 +1231,9 @@ class infolog_bo */ var $_vtodo2status = array( 'NEEDS-ACTION' => 'not-started', + 'NEEDS ACTION' => 'not-started', 'IN-PROCESS' => 'ongoing', + 'IN PROCESS' => 'ongoing', 'COMPLETED' => 'done', 'CANCELLED' => 'cancelled', ); @@ -1285,4 +1289,128 @@ class infolog_bo } return 'ongoing'; } + + /** + * Get the Parent ID of an InfoLog entry + * + * @param string $_guid + * @return string parentID + */ + function getParentID($_guid) + { + #Horde::logMessage("getParentID($_guid)", __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $parentID = False; + $myfilter = array('col_filter' => array('info_uid'=>$_guid)) ; + if ($_guid && ($found=$this->search($myfilter)) && ($uidmatch = array_shift($found))) { + $parentID = $uidmatch['info_id']; + }; + return $parentID; + } + + /** + * Try to find a matching db entry + * + * @param array $egwData the vTODO 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) + */ + function findVTODO($egwData, $relax=false) + { + $myfilter = array('col_filter' => array('info_uid'=>$egwData['info_uid'])) ; + if ($egwData['info_uid'] + && ($found = $this->search($myfilter)) + && ($uidmatch = array_shift($found))) + { + return $uidmatch['info_id']; + }; + unset($egwData['info_uid']); + + $filter = array(); + + $description = ''; + if (!empty($egwData['info_des'])) { + $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $egwData['info_des'])); + unset($egwData['info_des']); + // 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) + { + return $found['info_id']; + } + } + unset($egwData['info_id']); + + // priority does not need to match + unset($egwData['info_priority']); + + $filter['col_filter'] = $egwData; + + if($foundItems = $this->search($filter)) { + if(count($foundItems) > 0) { + $itemIDs = array_keys($foundItems); + return $itemIDs[0]; + } + } + + $filter = array(); + + if (!$relax && strlen($description)) { + $filter['search'] = $description; + } + + $filter['col_filter'] = $egwData; + + // search for date only match + unset($filter['col_filter']['info_startdate']); + unset($filter['col_filter']['info_datecompleted']); + + // 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; + } + // 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($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); + } + return false; + } } diff --git a/infolog/inc/class.infolog_hooks.inc.php b/infolog/inc/class.infolog_hooks.inc.php index 28dbd9fb81..d823f26f0b 100644 --- a/infolog/inc/class.infolog_hooks.inc.php +++ b/infolog/inc/class.infolog_hooks.inc.php @@ -331,6 +331,7 @@ class infolog_hooks private static function all_cats() { $categories = new categories('','infolog'); + $accountId = $GLOBALS['egw_info']['user']['account_id']; foreach((array)$categories->return_sorted_array(0,False,'','','',true) as $cat) { @@ -340,6 +341,14 @@ class infolog_hooks { $s .= ' ♦'; } + elseif ($cat['owner'] != $accountId) + { + $s .= '<' . $GLOBALS['egw']->accounts->id2name($cat['owner'], 'account_fullname') . '>'; + } + elseif ($cat['access'] == 'private') + { + $s .= ' ♥'; + } $sel_options[$cat['id']] = $s; // 0.9.14 only } return $sel_options; diff --git a/infolog/inc/class.infolog_ical.inc.php b/infolog/inc/class.infolog_ical.inc.php index 5dd4609b77..a33e63a7d3 100644 --- a/infolog/inc/class.infolog_ical.inc.php +++ b/infolog/inc/class.infolog_ical.inc.php @@ -4,15 +4,14 @@ * * @link http://www.egroupware.org * @author Lars Kneschke + * @author Joerg Lehrke * @package infolog * @subpackage syncml * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ -require_once EGW_API_INC.'/horde/Horde/iCalendar.php'; -require_once EGW_API_INC.'/horde/Horde/iCalendar/vnote.php'; -require_once EGW_API_INC.'/horde/Horde/iCalendar/vtodo.php'; +require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php'; /** * InfoLog: Create and parse iCal's @@ -20,17 +19,24 @@ require_once EGW_API_INC.'/horde/Horde/iCalendar/vtodo.php'; */ class infolog_ical extends infolog_bo { + /** + * @var array conversion of the priority egw => ical + */ var $egw_priority2vcal_priority = array( - 0 => 3, - 1 => 2, - 2 => 1, - 3 => 1, + 0 => 9, // low + 1 => 5, // normal + 2 => 3, // high + 3 => 1, // urgent ); + /** + * @var array conversion of the priority ical => egw + */ var $vcal_priority2egw_priority = array( - 1 => 2, - 2 => 1, - 3 => 0, + 9 => 0, 8 => 0, 7 => 0, // low + 6 => 1, 5 => 1, 4 => 1, 0 => 1, // normal + 3 => 2, 2 => 2, // high + 1 => 3, // urgent ); /** @@ -41,6 +47,32 @@ class infolog_ical extends infolog_bo var $productManufacturer = 'file'; var $productName = ''; + /** + * Shall we use the UID extensions of the description field? + * + * @var boolean + */ + var $uidExtension = false; + + /** + * Client CTCap Properties + * + * @var array + */ + var $clientProperties; + + /** + * Constructor + * + * @param array $_clientProperties client properties + */ + function __construct(&$_clientProperties = array()) + { + parent::__construct(); + + $this->clientProperties = $_clientProperties; + } + /** * Exports one InfoLog tast to an iCalendar VTODO * @@ -53,9 +85,36 @@ class infolog_ical extends infolog_bo { $taskData = $this->read($_taskID); - $taskData = $GLOBALS['egw']->translation->convert($taskData, $GLOBALS['egw']->translation->charset(), 'UTF-8'); + if ($taskData['info_id_parent']) + { + $parent = $this->read($taskData['info_id_parent']); + $taskData['info_id_parent'] = $parent['info_uid']; + } + else + { + $taskData['info_id_parent'] = ''; + } - $taskGUID = $GLOBALS['egw']->common->generate_uid('infolog_task',$_taskID); + if ($this->uidExtension) + { + if (!preg_match('/\[UID:.+\]/m', $taskData['info_des'])) + { + $taskData['info_des'] .= "\n[UID:" . $taskData['info_uid'] . "]"; + if ($taskData['info_id_parent'] != '') + { + $taskData['info_des'] .= "\n[PARENT_UID:" . $taskData['info_id_parent'] . "]"; + } + } + } + + if (!empty($taskData['info_cat'])) + { + $cats = $this->get_categories(array($taskData['info_cat'])); + $taskData['info_cat'] = $cats[0]; + } + + $taskData = $GLOBALS['egw']->translation->convert($taskData, + $GLOBALS['egw']->translation->charset(), 'UTF-8'); $vcal = new Horde_iCalendar; $vcal->setAttribute('VERSION',$_version); @@ -63,35 +122,91 @@ class infolog_ical extends infolog_bo $vevent = Horde_iCalendar::newComponent('VTODO',$vcal); - // set fields that may contain non-ascii chars and encode them if necessary - foreach(array( - 'SUMMARY' => $taskData['info_subject'], - 'DESCRIPTION' => $taskData['info_des'], - 'LOCATION' => $taskData['info_location'], - ) as $field => $value) + if (!isset($this->clientProperties['SUMMARY']['Size'])) { - $vevent->setAttribute($field,$value); - $options = array(); - if($this->productManufacturer != 'GroupDAV' && preg_match('/([\000-\012\015\016\020-\037\075])/',$value)) + // make SUMMARY a required field + $this->clientProperties['SUMMARY']['Size'] = 0xFFFF; + $this->clientProperties['SUMMARY']['NoTruncate'] = false; + } + // set fields that may contain non-ascii chars and encode them if necessary + foreach (array( + 'SUMMARY' => $taskData['info_subject'], + 'DESCRIPTION' => $taskData['info_des'], + 'LOCATION' => $taskData['info_location'], + 'RELATED-TO' => $taskData['info_id_parent'], + 'UID' => $taskData['info_uid'], + 'CATEGORIES' => $taskData['info_cat'], + ) as $field => $value) + { + if (isset($this->clientProperties[$field]['Size'])) { - $options['ENCODING'] = 'QUOTED-PRINTABLE'; + $size = $this->clientProperties[$field]['Size']; + $noTruncate = $this->clientProperties[$field]['NoTruncate']; + #Horde::logMessage("VTODO $field Size: $size, NoTruncate: " . + # ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); } - if($this->productManufacturer != 'GroupDAV' && preg_match('/([\177-\377])/',$value)) + else + { + $size = -1; + $noTruncate = false; + } + $cursize = strlen($value); + if (($size > 0) && $cursize > $size) + { + if ($noTruncate) + { + Horde::logMessage("VTODO $field omitted due to maximum size $size", + __FILE__, __LINE__, PEAR_LOG_WARNING); + continue; // skip field + } + // truncate the value to size + $value = substr($value, 0, $size -1); + #Horde::logMessage("VTODO $field truncated to maximum size $size", + # __FILE__, __LINE__, PEAR_LOG_INFO); + } + + if (empty($value) && ($size < 0 || $noTruncate)) continue; + + if ($field == 'RELATED-TO') + { + $options = array('RELTYPE' => 'PARENT'); + } + else + { + $options = array(); + } + + /*if(preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + }*/ + if ($this->productManufacturer != 'groupdav' + && preg_match('/([\177-\377])/',$value)) { $options['CHARSET'] = 'UTF-8'; } - if ($options) $vevent->setParameter($field, $options); + $vevent->setAttribute($field, $value, $options); } - if($taskData['info_startdate']) + + if ($taskData['info_startdate']) + { $vevent->setAttribute('DTSTART',$taskData['info_startdate']); - if($taskData['info_enddate']) - $vevent->setAttribute('DUE',$taskData['info_enddate']); - if($taskData['info_datecompleted']) + } + + if ($taskData['info_enddate']) + { + $parts = @getdate($taskData['info_enddate']); + $value = @mktime(12, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); + $vevent->setAttribute('DUE', $value); + } + + if ($taskData['info_datecompleted']) + { $vevent->setAttribute('COMPLETED',$taskData['info_datecompleted']); + } + $vevent->setAttribute('DTSTAMP',time()); $vevent->setAttribute('CREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_task',$_taskID,'add')); $vevent->setAttribute('LAST-MODIFIED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_task',$_taskID,'modify')); - $vevent->setAttribute('UID',$taskData['info_uid']); $vevent->setAttribute('CLASS',$taskData['info_access'] == 'public' ? 'PUBLIC' : 'PRIVATE'); $vevent->setAttribute('STATUS',$this->status2vtodo($taskData['info_status'])); // we try to preserv the original infolog status as X-INFOLOG-STATUS, so we can restore it, if the user does not modify STATUS @@ -99,16 +214,12 @@ class infolog_ical extends infolog_bo $vevent->setAttribute('PERCENT-COMPLETE',$taskData['info_percent']); $vevent->setAttribute('PRIORITY',$this->egw_priority2vcal_priority[$taskData['info_priority']]); - if (!empty($taskData['info_cat'])) - { - $cats = $this->get_categories(array($taskData['info_cat'])); - $vevent->setAttribute('CATEGORIES', $cats[0]); - } - //error_log("\n\nexportvcal\n". print_r($vcal,true)); - //error_log("\n\nexportvcal\n". print_r($vevent,true)); + $vcal->addComponent($vevent); - error_log("\n\nexportvcal from infolog\n"); - return $vcal->exportvCalendar(); + + $retval = $vcal->exportvCalendar(); + Horde::logMessage("exportVTODO:\n" . print_r($retval, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $retval; } /** @@ -116,104 +227,143 @@ 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 * @return int|boolean integer info_id or false on error */ - function importVTODO(&$_vcalData, $_taskID=-1) + function importVTODO(&$_vcalData, $_taskID=-1, $merge=false) { - if(!$taskData = $this->vtodotoegw($_vcalData,$_taskID)) - { - return false; - } + if (!$taskData = $this->vtodotoegw($_vcalData,$_taskID)) return false; + // we suppose that a not set status in a vtodo means that the task did not started yet - if(empty($taskData['info_status'])) + if (empty($taskData['info_status'])) { $taskData['info_status'] = 'not-started'; } + + if (empty($taskData['info_datecompleted'])) + { + $taskData['info_datecompleted'] = 0; + } + return $this->write($taskData); } - function searchVTODO($_vcalData, $contentID=null) - { - if(!$egwData = $this->vtodotoegw($_vcalData)) { - return false; - } - - $myfilter = array('col_filter' => array('info_uid'=>$egwData['info_uid'])) ; - if ($egwData['info_uid'] && ($found=parent::search($myfilter)) && ($uidmatch = array_shift($found))) + /** + * Search a matching infolog entry for the VTODO data + * + * @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 + */ + function searchVTODO($_vcalData, $contentID=null, $relax=false) { + $result = false; + + if (($egwData = $this->vtodotoegw($_vcalData))) { - return $uidmatch['info_id']; - }; - unset($egwData['info_uid']); - - if ($contentID) { - $egwData['info_id'] = $contentID; - } - - #unset($egwData['info_priority']); - - $filter = array('col_filter' => $egwData); - if($foundItems = $this->search($filter)) { - if(count($foundItems) > 0) { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; + if ($contentID) + { + $egwData['info_id'] = $contentID; } + $result = $this->findVTODO($egwData, $relax); } - - return false; + return $result; } - function vtodotoegw($_vcalData,$_taskID=-1) + /** + * Convert VTODO into a eGW infolog entry + * + * @param string $_vcalData VTODO data + * @param int $_taskID=-1 infolog_id of the entry + * @return array infolog entry or false on error + */ + function vtodotoegw($_vcalData, $_taskID=-1) { $vcal = new Horde_iCalendar; - if(!$vcal->parsevCalendar($_vcalData)) + if (!($vcal->parsevCalendar($_vcalData))) return false; + + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { - return FALSE; + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } + else + { + $minimum_uid_length = 8; } $components = $vcal->getComponents(); - if(count($components) > 0) + foreach ($components as $component) { - $component = $components[0]; - if(is_a($component, 'Horde_iCalendar_vtodo')) + if (is_a($component, 'Horde_iCalendar_vtodo')) { $taskData = array(); - if($_taskID > 0) + $taskData['info_type'] = 'task'; + + if ($_taskID > 0) { $taskData['info_id'] = $_taskID; } - foreach($component->_attributes as $attributes) + foreach ($component->_attributes as $attributes) { - switch($attributes['name']) + //$attributes['value'] = trim($attributes['value']); + if (empty($attributes['value'])) continue; + switch ($attributes['name']) { case 'CLASS': - $taskData['info_access'] = strtolower($attributes['value']); + $taskData['info_access'] = strtolower($attributes['value']); break; + case 'DESCRIPTION': - $taskData['info_des'] = $attributes['value']; + $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']; + $taskData['info_location'] = $attributes['value']; break; + case 'DUE': - $taskData['info_enddate'] = $attributes['value']; + // 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']; + $taskData['info_startdate'] = $attributes['value']; break; + case 'PRIORITY': - if (1 <= $attributes['value'] && $attributes['value'] <= 3) - { - $taskData['info_priority'] = $this->vcal_priority2egw_priority[$attributes['value']]; - } - else - { - $taskData['info_priority'] = 1; // default = normal + if (1 <= $attributes['value'] && $attributes['value'] <= 3) { + $taskData['info_priority'] = $this->vcal_priority2egw_priority[$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) @@ -223,28 +373,26 @@ class infolog_ical extends infolog_bo $taskData['info_status'] = $this->vtodo2status($attributes['value'], $attr['name'] == 'X-INFOLOG-STATUS' ? $attr['value'] : null); break; + case 'SUMMARY': - $taskData['info_subject'] = $attributes['value']; + $taskData['info_subject'] = $attributes['value']; + break; + + case 'RELATED-TO': + $taskData['info_id_parent'] = $this->getParentID($attributes['value']); break; case 'CATEGORIES': $cats = $this->find_or_add_categories(explode(',', $attributes['value'])); $taskData['info_cat'] = $cats[0]; break; + case 'UID': - $taskData['info_uid'] = $attributes['value']; - if ($_taskID <= 0 && !empty($attributes['value']) && ($uid_task = $this->read($attributes['value']))) - { - $taskData['info_id'] = $uid_task['id']; - unset($uid_task); + if (strlen($attributes['value']) >= $minimum_uid_length) { + $taskData['info_uid'] = $attributes['value']; } - // not use weak uids that might come from syncml clients - if (isset($event['uid']) && (strlen($event['uid']) < 20 || is_numeric($event['uid']))) - { - unset ($event['uid']); - } - break; + case 'PERCENT-COMPLETE': $taskData['info_percent'] = (int) $attributes['value']; break; @@ -254,18 +402,28 @@ class infolog_ical extends infolog_bo # 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; } } - return FALSE; + return false; } + /** + * Export an infolog entry as VNOTE + * + * @param int $_noteID the infolog_id of the entry + * @param string $_type content type (e.g. text/plain) + * @return string VNOTE representation of the infolog entry + */ function exportVNOTE($_noteID, $_type) { $note = $this->read($_noteID); - $note = $GLOBALS['egw']->translation->convert($note, $GLOBALS['egw']->translation->charset(), 'UTF-8'); + $note = $GLOBALS['egw']->translation->convert($note, + $GLOBALS['egw']->translation->charset(), 'UTF-8'); - switch($_type) + switch ($_type) { case 'text/plain': $txt = $note['info_subject']."\n\n".$note['info_des']; @@ -273,66 +431,101 @@ class infolog_ical extends infolog_bo break; case 'text/x-vnote': - $noteGUID = $GLOBALS['egw']->common->generate_uid('infolog_note',$_noteID); $vnote = new Horde_iCalendar_vnote(); + $options = array('CHARSET' => 'UTF-8'); $vNote->setAttribute('VERSION', '1.1'); - $vnote->setAttribute('SUMMARY',$note['info_subject']); - $vnote->setAttribute('BODY',$note['info_des']); - if($note['info_startdate']) + foreach (array( 'SUMMARY' => $note['info_subject'], + 'BODY' => $note['info_des'], + ) as $field => $value) + { + $vnote->setAttribute($field, $value); + if ($this->productManufacturer != 'groupdav' + && preg_match('/([\177-\377])/', $value)) + { + $vevent->setParameter($field, $options); + } + } + if ($note['info_startdate']) + { $vnote->setAttribute('DCREATED',$note['info_startdate']); + } $vnote->setAttribute('DCREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'add')); $vnote->setAttribute('LAST-MODIFIED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'modify')); + if (!empty($note['info_cat'])) { $cats = $this->get_categories(array($note['info_cat'])); - $vnote->setAttribute('CATEGORIES', $cats[0]); + $value = $cats[0]; + $vnote->setAttribute('CATEGORIES', $value); + if ($this->productManufacturer != 'groupdav' + && preg_match('/([\177-\377])/', $value)) + { + $vevent->setParameter('CATEGORIES', $options); + } } - #$vnote->setAttribute('UID',$noteGUID); #$vnote->setAttribute('CLASS',$taskData['info_access'] == 'public' ? 'PUBLIC' : 'PRIVATE'); - #$options = array('CHARSET' => 'UTF-8','ENCODING' => 'QUOTED-PRINTABLE'); - #$vnote->setParameter('SUMMARY', $options); - #$vnote->setParameter('DESCRIPTION', $options); - return $vnote->exportvCalendar(); break; } return false; } - function importVNOTE(&$_vcalData, $_type, $_noteID = -1) + /** + * Import a VNOTE component of an iCal + * + * @param string $_vcalData + * @param string $_type content type (eg.g text/plain) + * @param int $_taskID=-1 info_id, default -1 = new entry + * @param boolean $merge=false merge data with existing entry + * @return int|boolean integer info_id or false on error + */ + function importVNOTE(&$_vcalData, $_type, $_noteID = -1, $merge=false) { - if(!$note = $this->vnotetoegw($_vcalData, $_type)) - { - return false; - } + if (!($note = $this->vnotetoegw($_vcalData, $_type))) return false; - if($_noteID > 0) - { - $note['info_id'] = $_noteID; - } + if($_noteID > 0) $note['info_id'] = $_noteID; - if(empty($note['info_status'])) { - $note['info_status'] = 'done'; - } + if (empty($note['info_status'])) $note['info_status'] = 'done'; #_debug_array($taskData);exit; return $this->write($note); } + /** + * Search a matching infolog entry for the VNOTE data + * + * @param string $_vcalData VNOTE + * @param int $contentID=null infolog_id (or null, if unkown) + * @return infolog_id of a matching entry or false, if nothing was found + */ function searchVNOTE($_vcalData, $_type, $contentID=null) { - if(!$note = $this->vnotetoegw($_vcalData,$_type)) { - return false; - } - if ($contentID) { - $note['info_id'] = $contentID; + if (!($note = $this->vnotetoegw($_vcalData,$_type))) return false; + + 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 = array('col_filter' => $note); - if($foundItems = $this->search($filter)) { - if(count($foundItems) > 0) { + $filter['col_filter'] = $note; + + if (($foundItems = $this->search($filter))) + { + if (count($foundItems) > 0) + { $itemIDs = array_keys($foundItems); return $itemIDs[0]; } @@ -341,9 +534,16 @@ class infolog_ical extends infolog_bo return false; } + /** + * Convert VTODO into a eGW infolog entry + * + * @param string $_data VNOTE data + * @param string $_type content type (eg.g text/plain) + * @return array infolog entry or false on error + */ function vnotetoegw($_data, $_type) { - switch($_type) + switch ($_type) { case 'text/plain': $note = array(); @@ -360,7 +560,7 @@ class infolog_ical extends infolog_bo else { // should better be imported as subject, but causes duplicates - // TODO: should be qexamined + // TODO: should be examined $note['info_des'] = $txt; } @@ -369,34 +569,31 @@ class infolog_ical extends infolog_bo case 'text/x-vnote': $vnote = new Horde_iCalendar; - if (!$vcal->parsevCalendar($_data)) - { - return FALSE; - } + if (!$vcal->parsevCalendar($_data)) return false; + $components = $vnote->getComponent(); - if(count($components) > 0) + foreach ($components as $component) { - $component = $components[0]; - if(is_a($component, 'Horde_iCalendar_vnote')) + if (is_a($component, 'Horde_iCalendar_vnote')) { $note = array(); $note['info_type'] = 'note'; - foreach($component->_attributes as $attribute) + foreach ($component->_attributes as $attribute) { switch ($attribute['name']) { case 'BODY': $note['info_des'] = $attribute['value']; break; + case 'SUMMARY': $note['info_subject'] = $attribute['value']; break; + case 'CATEGORIES': - { - $cats = $this->find_or_add_categories(explode(',', $attribute['value'])); - $note['info_cat'] = $cats[0]; - } + $cats = $this->find_or_add_categories(explode(',', $attribute['value'])); + $note['info_cat'] = $cats[0]; break; } } @@ -404,7 +601,7 @@ class infolog_ical extends infolog_bo return $note; } } - return FALSE; + return false; } /** @@ -415,11 +612,41 @@ class infolog_ical extends infolog_bo * @param string $_productManufacturer * @param string $_productName */ - function setSupportedFields($_productManufacturer='file', $_productName='') + function setSupportedFields($_productManufacturer='', $_productName='') { - // save them vor later use - $this->productManufacturer = $_productManufacturer; - $this->productName = $_productName; + $state = &$_SESSION['SyncML.state']; + if (isset($state)) + { + $deviceInfo = $state->getClientDeviceInfo(); + } + + // store product manufacturer and name, to be able to use it elsewhere + if ($_productManufacturer) + { + $this->productManufacturer = strtolower($_productManufacturer); + $this->productName = strtolower($_productName); + } + + if (isset($deviceInfo) && is_array($deviceInfo)) + { + if (!isset($this->productManufacturer) + || $this->productManufacturer == '' + || $this->productManufacturer == 'file') + { + $this->productManufacturer = strtolower($deviceInfo['manufacturer']); + } + if (!isset($this->productName) || $this->productName == '') + { + $this->productName = strtolower($deviceInfo['model']); + } + if (isset($deviceInfo['uidExtension']) + && $deviceInfo['uidExtension']) + { + $this->uidExtension = true; + } + } + + Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); + } } - diff --git a/infolog/inc/class.infolog_sif.inc.php b/infolog/inc/class.infolog_sif.inc.php index 8c24beda10..c35e4e61ac 100644 --- a/infolog/inc/class.infolog_sif.inc.php +++ b/infolog/inc/class.infolog_sif.inc.php @@ -4,13 +4,14 @@ * * @link http://www.egroupware.org * @author Lars Kneschke + * @author Joerg Lehrke * @package infolog * @subpackage syncml * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ -require_once EGW_API_INC.'/horde/Horde/iCalendar.php'; +require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php'; /** * InfoLog: Create and parse SIF @@ -38,58 +39,88 @@ class infolog_sif extends infolog_bo // mappings for SIFTask to InfologTask var $_sifTaskMapping = array( - 'ActualWork' => '', + 'ActualWork' => '', 'BillingInformation' => '', - 'Body' => 'info_des', - 'Categories' => 'info_cat', - 'Companies' => '', - 'Complete' => '', - 'DateCompleted' => 'info_datecompleted', - 'DueDate' => 'info_enddate', - 'Importance' => 'info_priority', - 'IsRecurring' => '', - 'Mileage' => '', - 'PercentComplete' => 'info_percent', - 'ReminderSet' => '', - 'ReminderTime' => '', - 'Sensitivity' => 'info_access', - 'StartDate' => 'info_startdate', - 'Status' => 'info_status', - 'Subject' => 'info_subject', - 'TeamTask' => '', - 'TotalWork' => '', - 'RecurrenceType' => '', - 'Interval' => '', - 'MonthOfYear' => '', - 'DayOfMonth' => '', - 'DayOfWeekMask' => '', - 'Instance' => '', - 'PatternStartDate' => '', - 'NoEndDate' => '', - 'PatternEndDate' => '', - 'Occurrences' => '', + 'Body' => 'info_des', + 'Categories' => 'info_cat', + 'Companies' => '', + 'Complete' => 'complete', + 'DateCompleted' => 'info_datecompleted', + 'DueDate' => 'info_enddate', + 'Importance' => 'info_priority', + 'IsRecurring' => '', + 'Mileage' => '', + 'PercentComplete' => 'info_percent', + 'ReminderSet' => '', + 'ReminderTime' => '', + 'Sensitivity' => 'info_access', + 'StartDate' => 'info_startdate', + 'Status' => 'info_status', + 'Subject' => 'info_subject', + 'TeamTask' => '', + 'TotalWork' => '', + 'RecurrenceType' => '', + 'Interval' => '', + 'MonthOfYear' => '', + 'DayOfMonth' => '', + 'DayOfWeekMask' => '', + 'Instance' => '', + 'PatternStartDate' => '', + 'NoEndDate' => '', + 'PatternEndDate' => '', + 'Occurrences' => '', ); + // standard headers + const xml_decl = ''; + const SIF_decl = '1.1'; + + /** + * name and version of the sync-client + * + * @var string + */ + var $productName = 'mozilla plugin'; + var $productSoftwareVersion = '0.3'; + + /** + * Shall we use the UID extensions of the description field? + * + * @var boolean + */ + var $uidExtension = false; - function startElement($_parser, $_tag, $_attributes) { + function startElement($_parser, $_tag, $_attributes) + { + // nothing to do } - function endElement($_parser, $_tag) { - error_log("infolog: tag=$_tag data=".trim($this->sifData)); - if(!empty($this->_currentSIFMapping[$_tag])) { + function endElement($_parser, $_tag) + { + #error_log("infolog: tag=$_tag data=".trim($this->sifData)); + if (!empty($this->_currentSIFMapping[$_tag])) + { $this->_extractedSIFData[$this->_currentSIFMapping[$_tag]] = trim($this->sifData); } unset($this->sifData); } - function characterData($_parser, $_data) { + function characterData($_parser, $_data) + { $this->sifData .= $_data; } - function siftoegw($_sifData, $_sifType) { + /** + * Convert SIF data into a eGW infolog entry + * + * @param string $sifData the SIF data + * @param string $_sifType type (note/task) + * @return array infolog entry or false on error + */ + function siftoegw($sifData, $_sifType) + { $sysCharSet = $GLOBALS['egw']->translation->charset(); - $sifData = base64_decode($_sifData); #$tmpfname = tempnam('/tmp/sync/contents','sift_'); @@ -115,29 +146,44 @@ class infolog_sif extends infolog_bo xml_set_element_handler($this->xml_parser, "startElement", "endElement"); xml_set_character_data_handler($this->xml_parser, "characterData"); $this->strXmlData = xml_parse($this->xml_parser, $sifData); - if(!$this->strXmlData) { + + if (!$this->strXmlData) + { error_log(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($this->xml_parser)), xml_get_current_line_number($this->xml_parser))); return false; } - if(!array($this->_extractedSIFData)) { - return false; - } + if (!array($this->_extractedSIFData)) return false; - switch($_sifType) { + switch ($_sifType) + { case 'task': $taskData = array(); $vcal = new Horde_iCalendar; $taskData['info_type'] = 'task'; + $taskData['info_status'] = 'not-started'; - foreach($this->_extractedSIFData as $key => $value) { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) + { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } + else + { + $minimum_uid_length = 8; + } + + foreach ($this->_extractedSIFData as $key => $value) + { + $value = preg_replace('/<\!\[CDATA\[(.+)\]\]>/Usim', '$1', $value); $value = $GLOBALS['egw']->translation->convert($value, 'utf-8', $sysCharSet); - error_log("infolog key=$key => value=$value"); + #error_log("infolog key=$key => value=$value"); + if (empty($value)) continue; - switch($key) { + switch($key) + { case 'info_access': $taskData[$key] = ((int)$value > 0) ? 'private' : 'public'; break; @@ -145,19 +191,18 @@ class infolog_sif extends infolog_bo case 'info_datecompleted': case 'info_enddate': case 'info_startdate': - if(!empty($value)) { + if (!empty($value)) + { $taskData[$key] = $vcal->_parseDateTime($value); // somehow the client always deliver a timestamp about 3538 seconds, when no startdate set. - if($taskData[$key] < 10000) - $taskData[$key] = ''; - } else { - $taskData[$key] = ''; + if ($taskData[$key] < 10000) unset($taskData[$key]); } break; case 'info_cat': - if (!empty($value)) { + if (!empty($value)) + { $categories = $this->find_or_add_categories(explode(';', $value)); $taskData['info_cat'] = $categories[0]; } @@ -168,8 +213,8 @@ class infolog_sif extends infolog_bo break; case 'info_status': - $taskData[$key] = ((int)$value == 2) ? 'done' : 'ongoing'; - switch($value) { + switch ($value) + { case '0': $taskData[$key] = 'not-started'; break; @@ -178,9 +223,20 @@ class infolog_sif extends infolog_bo break; case '2': $taskData[$key] = 'done'; + $taskData['info_percent'] = 100; + break; + case '3': + $taskData[$key] = 'waiting'; break; case '4': - $taskData[$key] = 'cancelled'; + if ($this->productName == 'blackberry plug-in') + { + $taskData[$key] = 'deferred'; + } + else + { + $taskData[$key] = 'cancelled'; + } break; default: $taskData[$key] = 'ongoing'; @@ -188,11 +244,35 @@ class infolog_sif extends infolog_bo } break; + case 'complete': + $taskData['info_status'] = 'done'; + $taskData['info_percent'] = 100; + break; + + case 'info_des': + // extract our UID and PARENT_UID information + if (preg_match('/\s*\[UID:(.+)?\]/Usm', $value, $matches)) + { + if (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 (strlen($matches[1]) >= $minimum_uid_length) + { + $taskData['info_id_parent'] = $this->getParentID($matches[1]); + } + //$value = str_replace($matches[0], '', $value); + } + default: $taskData[$key] = $value; break; } - error_log("infolog task key=$key => value=".$taskData[$key]); + #error_log("infolog task key=$key => value=" . $taskData[$key]); } return $taskData; @@ -203,28 +283,32 @@ class infolog_sif extends infolog_bo $noteData['info_type'] = 'note'; $vcal = new Horde_iCalendar; - foreach($this->_extractedSIFData as $key => $value) + foreach ($this->_extractedSIFData as $key => $value) { + $value = preg_replace('/<\!\[CDATA\[(.+)\]\]>/Usim', '$1', $value); $value = $GLOBALS['egw']->translation->convert($value, 'utf-8', $sysCharSet); - error_log("infolog client key=$key => value=".$value); + #error_log("infolog client key=$key => value=" . $value); switch ($key) { case 'info_startdate': - if(!empty($value)) { + if (!empty($value)) + { $noteData[$key] = $vcal->_parseDateTime($value); // somehow the client always deliver a timestamp about 3538 seconds, when no startdate set. - if($noteData[$key] < 10000) - $noteData[$key] = ''; - } else { + if ($noteData[$key] < 10000) $noteData[$key] = ''; + } + else + { $noteData[$key] = ''; } break; case 'info_cat': - if (!empty($value)) { + if (!empty($value)) + { $categories = $this->find_or_add_categories(explode(';', $value)); - $taskData['info_cat'] = $categories[0]; + $noteData['info_cat'] = $categories[0]; } break; @@ -232,7 +316,7 @@ class infolog_sif extends infolog_bo $noteData[$key] = $value; break; } - error_log("infolog note key=$key => value=".$noteData[$key]); + #error_log("infolog note key=$key => value=".$noteData[$key]); } return $noteData; break; @@ -243,17 +327,33 @@ class infolog_sif extends infolog_bo } } - function searchSIF($_sifData, $_sifType, $contentID=null) { - if(!$egwData = $this->siftoegw($_sifData, $_sifType)) { - return false; - } - if ($contentID) { - $egwData['info_id'] = $contentID; - } + /** + * Search for SIF data a matching infolog entry + * + * @param string $sifData the SIF data + * @param string $_sifType type (note/task) + * @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 searchSIF($_sifData, $_sifType, $contentID=null, $relax=false) + { + if (!($egwData = $this->siftoegw($_sifData, $_sifType))) return false; - $filter = array('col_filter' => $egwData); - if($foundItems = $this->search($filter)) { - if(count($foundItems) > 0) { + 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]; } @@ -262,46 +362,119 @@ class infolog_sif extends infolog_bo return false; } - function addSIF($_sifData, $_id, $_sifType) { - if(!$egwData = $this->siftoegw($_sifData, $_sifType)) { - return false; - } + /** + * Add SIF data entry + * + * @param string $sifData the SIF data + * @param string $_sifType type (note/task) + * @param boolean $merge=false reserved for future use + * @return infolog_id of the new entry or false, for errors + */ + function addSIF($_sifData, $_id, $_sifType, $merge=false) + { + if (!($egwData = $this->siftoegw($_sifData, $_sifType))) return false; - if($_id > 0) - $egwData['info_id'] = $_id; + if ($_id > 0) $egwData['info_id'] = $_id; + + if (empty($taskData['info_datecompleted'])) + { + $taskData['info_datecompleted'] = 0; + } $egwID = $this->write($egwData, false); return $egwID; } - function getSIF($_id, $_sifType) { - switch($_sifType) { + + /** + * Export an infolog entry as SIF data + * + * @param int $_id the infolog_id of the entry + * @param string $_sifType type (note/task) + * @return string SIF representation of the infolog entry + */ + function getSIF($_id, $_sifType) + { + $sysCharSet = $GLOBALS['egw']->translation->charset(); + + switch($_sifType) + { case 'task': - if($taskData = $this->read($_id)) { - $sysCharSet = $GLOBALS['egw']->translation->charset(); - $vcal = new Horde_iCalendar; + if (($taskData = $this->read($_id))) + { + $vcal = new Horde_iCalendar('1.0'); - $sifTask = ''; - - foreach($this->_sifTaskMapping as $sifField => $egwField) + if ($taskData['info_id_parent']) { - if(empty($egwField)) continue; + $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) { + switch ($sifField) + { + + case 'Complete': + // is handled with DateCompleted + break; + case 'DateCompleted': - case 'DueDate': - case 'StartDate': - if(!empty($value)) { - $value = $vcal->_exportDateTime($value); + 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>"; } - $sifTask .= "<$sifField>$value"; break; case 'Importance': - if($value > 3) $value = 3; + if ($value > 3) $value = 3; $sifTask .= "<$sifField>$value"; break; @@ -311,20 +484,26 @@ class infolog_sif extends infolog_bo break; case 'Status': - switch($value) { + 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; - case 'ongoing': - $value = '1'; - break; - default: + default: //ongoing $value = 1; break; } @@ -332,102 +511,69 @@ class infolog_sif extends infolog_bo break; case 'Categories': - if (!empty($value)) + if (!empty($value) && $value) { $value = implode('; ', $this->get_categories(array($value))); $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); } - $sifTask .= "<$sifField>$value"; - break; + else + { + break; + } default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); $sifTask .= "<$sifField>$value"; break; } } - - $sifTask .= '00'; - return base64_encode($sifTask); - -/* return base64_encode(" - 0 - - - - - 0 - - - 1 - 0 - - 0 - 0 - - 0 - 45001231T230000Z - 3 - TARAAA3 - 0 - 0 - 1 - 1 - 0 - 0 - 4 - 0 - 20060320T230000Z - 1 - - 10 - - "); */ + $sifTask .= '00'; + return $sifTask; } break; case 'note': - if($taskData = $this->read($_id)) { - $sysCharSet = $GLOBALS['egw']->translation->charset(); - $vcal = new Horde_iCalendar; + if (($taskData = $this->read($_id))) + { + $vcal = new Horde_iCalendar('1.0'); - $sifNote = ''; + $sifNote = self::xml_decl . "\n" . self::SIF_decl; - foreach($this->_sifNoteMapping as $sifField => $egwField) + foreach ($this->_sifNoteMapping as $sifField => $egwField) { if(empty($egwField)) continue; $value = $GLOBALS['egw']->translation->convert($taskData[$egwField], $sysCharSet, 'utf-8'); - switch($sifField) { + switch ($sifField) + { case 'Date': - if(!empty($value)) { + if (!empty($value)) + { $value = $vcal->_exportDateTime($value); } $sifNote .= "<$sifField>$value"; break; - case 'Body': - $value = $GLOBALS['egw']->translation->convert($taskData['info_subject'], $sysCharSet, 'utf-8') . "\n" . $value; - $sifNote .= "<$sifField>$value"; - break; - case 'Categories': if (!empty($value)) { $value = implode('; ', $this->get_categories(array($value))); $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); } - $sifNote .= "<$sifField>$value"; - break; - + else + { + break; + } default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); $sifNote .= "<$sifField>$value"; break; } } - - return base64_encode($sifNote); + $sifNote .= ''; + return $sifNote; } break; @@ -437,117 +583,35 @@ class infolog_sif extends infolog_bo } - function exportVTODO($_taskID, $_version) + /** + * Set the supported fields + * + * Currently we only store name and version, manucfacturer is always Funambol + * + * @param string $_productName + * @param string $_productSoftwareVersion + */ + function setSupportedFields($_productName='', $_productSoftwareVersion='') { - error_log(__METHOD__."called : $_version ,$_taskID"); - $taskData = $this->read($_taskID); + $state = &$_SESSION['SyncML.state']; + $deviceInfo = $state->getClientDeviceInfo(); - $taskData = $GLOBALS['egw']->translation->convert($taskData,$GLOBALS['egw']->translation->charset(),'UTF-8'); - - //_debug_array($taskData); - - $taskGUID = $GLOBALS['phpgw']->common->generate_uid('infolog_task',$_taskID); - - $vcal = new Horde_iCalendar; - $vcal->setAttribute('VERSION',$_version); - $vcal->setAttribute('METHOD','PUBLISH'); - - $vevent = Horde_iCalendar::newComponent('VTODO',$vcal); - - $options = array(); - - $vevent->setAttribute('SUMMARY',$taskData['info_subject']); - $vevent->setAttribute('DESCRIPTION',$taskData['info_des']); - if($taskData['info_startdate']) - $vevent->setAttribute('DTSTART',$taskData['info_startdate']); - if($taskData['info_enddate']) - $vevent->setAttribute('DUE',$taskData['info_enddate']); - $vevent->setAttribute('DTSTAMP',time()); - $vevent->setAttribute('CREATED',$GLOBALS['phpgw']->contenthistory->getTSforAction('infolog_task',$_taskID,'add')); - $vevent->setAttribute('LAST-MODIFIED',$GLOBALS['phpgw']->contenthistory->getTSforAction('infolog_task',$_taskID,'modify')); - $vevent->setAttribute('UID',$taskGUID); - $vevent->setAttribute('CLASS',(($taskData['info_access'] == 'public')?'PUBLIC':'PRIVATE')); - $vevent->setAttribute('STATUS',(($taskData['info_status'] == 'completed')?'COMPLETED':'NEEDS-ACTION')); - // 3=urgent => 1, 2=high => 2, 1=normal => 3, 0=low => 4 - $vevent->setAttribute('PRIORITY',4-$taskData['info_priority']); - - #$vevent->setAttribute('TRANSP','OPAQUE'); - # status - # ATTENDEE - - $options = array('CHARSET' => 'UTF-8','ENCODING' => 'QUOTED-PRINTABLE'); - $vevent->setParameter('SUMMARY', $options); - $vevent->setParameter('DESCRIPTION', $options); - - $vcal->addComponent($vevent); - - #print "
";
-		#print $vcal->exportvCalendar();
-		#print "
"; - - return $vcal->exportvCalendar(); - } - - function importVTODO(&$_vcalData, $_taskID=-1) - { - $botranslation = CreateObject('phpgwapi.translation'); - - $vcal = new Horde_iCalendar; - if(!$vcal->parsevCalendar($_vcalData)) + if (isset($deviceInfo) && is_array($deviceInfo)) { - return FALSE; - } - $components = $vcal->getComponents(); - if(count($components) > 0) - { - $component = $components[0]; - if(is_a($component, 'Horde_iCalendar_vtodo')) + if (isset($deviceInfo['uidExtension']) && + $deviceInfo['uidExtension']) { - if($_taskID>0) - $taskData['info_id'] = $_taskID; - - foreach($component->_attributes as $attributes) - { - #print $attributes['name'].' - '.$attributes['value'].'
'; - #$attributes['value'] = $GLOBALS['egw']->translation->convert($attributes['value'],'UTF-8'); - switch($attributes['name']) - { - case 'CLASS': - $taskData['info_access'] = strtolower($attributes['value']); - break; - case 'DESCRIPTION': - $taskData['info_des'] = $attributes['value']; - break; - case 'DUE': - $taskData['info_enddate'] = $attributes['value']; - break; - case 'DTSTART': - $taskData['info_startdate'] = $attributes['value']; - break; - case 'PRIORITY': - // 1 => 3=urgent, 2 => 2=high, 3 => 1=normal, 4 => 0=low - if (1 <= $attributes['value'] && $attributes['value'] <= 4) - { - $taskData['info_priority'] = 4 - $attributes['value']; - } - else - { - $taskData['info_priority'] = 1; // default = normal - } - break; - case 'STATUS': - $taskData['info_status'] = (strtolower($attributes['value']) == 'completed') ? 'done' : 'ongoing'; - break; - case 'SUMMARY': - $taskData['info_subject'] = $attributes['value']; - break; - } - } - #_debug_array($eventData);exit; - return $this->write($taskData); + $this->uidExtension = true; + } + } + // store product name and version, to be able to use it elsewhere + if ($_productName) + { + $this->productName = strtolower($_productName); + if (preg_match('/^[^\d]*(\d+\.?\d*)[\.|\d]*$/', $_productSoftwareVersion, $matches)) + { + $this->productSoftwareVersion = $matches[1]; } } - - return FALSE; } } diff --git a/infolog/inc/class.infolog_so.inc.php b/infolog/inc/class.infolog_so.inc.php index 56fd37819b..8c79ac7530 100644 --- a/infolog/inc/class.infolog_so.inc.php +++ b/infolog/inc/class.infolog_so.inc.php @@ -344,6 +344,12 @@ class infolog_so */ function read($info_id) // did _not_ ensure ACL { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } else { + $minimum_uid_length = 8; + } + //echo "

read($info_id) ".function_backtrace()."

\n"; if ($info_id && ((int)$info_id == $this->data['info_id'] || $info_id == $this->data['info_uid'])) { @@ -356,6 +362,14 @@ class infolog_so $this->init( ); return False; } + if (!$this->data['info_uid'] || strlen($this->data['info_uid']) < $minimum_uid_length) { + // entry without uid --> create one based on our info_id and save it + + $this->data['info_uid'] = $GLOBALS['egw']->common->generate_uid('infolog', $info_id); + $this->db->update($this->info_table, + array('info_uid' => $this->data['info_uid']), + array('info_id' => $this->data['info_id']), __LINE__,__FILE__); + } if (!is_array($this->data['info_responsible'])) { $this->data['info_responsible'] = $this->data['info_responsible'] ? explode(',',$this->data['info_responsible']) : array(); @@ -499,6 +513,12 @@ class infolog_so */ function write($values,$check_modified=0) // did _not_ ensure ACL { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } else { + $minimum_uid_length = 8; + } + //echo "soinfolog::write(,$check_modified) values="; _debug_array($values); $info_id = (int) $values['info_id']; @@ -540,12 +560,17 @@ class infolog_so $this->db->insert($this->info_table,$to_write,false,__LINE__,__FILE__); $info_id = $this->data['info_id'] = $this->db->get_last_insert_id($this->info_table,'info_id'); - if (!$this->data['info_uid']) // new entry without uid --> create one based on our info_id and save it - { - $this->data['info_uid'] = $GLOBALS['egw']->common->generate_uid('infolog',$info_id); - $this->db->update($this->info_table,array('info_uid'=>$this->data['info_uid']),array('info_id'=>$info_id),__LINE__,__FILE__); - } } + + if (!$this->data['info_uid'] || strlen($this->data['info_uid']) < $minimum_uid_length) { + // entry without uid --> create one based on our info_id and save it + + $this->data['info_uid'] = $GLOBALS['egw']->common->generate_uid('infolog', $info_id); + $this->db->update($this->info_table, + array('info_uid' => $this->data['info_uid']), + array('info_id' => $info_id), __LINE__,__FILE__); + } + //echo "

soinfolog.write values= "; _debug_array($values); // write customfields now @@ -775,6 +800,7 @@ class infolog_so if ($action == '' || $action == 'sp' || count($links)) { $sql_query = "FROM $this->info_table main $join WHERE ($filtermethod $pid $sql_query) $link_extra"; + #error_log("infolog.so.search:\n" . print_r($sql_query, true)); if ($this->db->Type == 'mysql' && $this->db->ServerInfo['version'] >= 4.0) { diff --git a/infolog/inc/class.infolog_tracking.inc.php b/infolog/inc/class.infolog_tracking.inc.php index 7871b58026..16d21a0256 100644 --- a/infolog/inc/class.infolog_tracking.inc.php +++ b/infolog/inc/class.infolog_tracking.inc.php @@ -186,6 +186,7 @@ class infolog_tracking extends bo_tracking */ function get_details($data) { + $header_done = false; $responsible = array(); if ($data['info_responsible']) { diff --git a/infolog/inc/class.infolog_ui.inc.php b/infolog/inc/class.infolog_ui.inc.php index eae3c2cc03..2784470fe5 100644 --- a/infolog/inc/class.infolog_ui.inc.php +++ b/infolog/inc/class.infolog_ui.inc.php @@ -654,6 +654,7 @@ class infolog_ui * * @param int|array $values=0 info_id (default _GET[info_id]) * @param string $referer='' + * @param boolean $closesingle=false */ function close($values=0,$referer='',$closesingle=false) { @@ -973,6 +974,7 @@ class infolog_ui } $parent = $this->bo->so->data; $content['info_id'] = $info_id = 0; + $content['info_uid'] = ''; // ensure that we have our own UID $content['info_owner'] = $this->user; $content['info_id_parent'] = $parent['info_id']; /* @@ -1432,7 +1434,6 @@ class infolog_ui #continue; } $message .= $bofelamimail->wordwrap($value,75,"\n"); - #$message .= $bodyAppend; } }