* @author Joerg Lehrke * @author Ralf Becker * @package infolog * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ use EGroupware\Api; use EGroupware\Api\Acl; /** * InfoLog: Create and parse iCal's */ class infolog_ical extends infolog_bo { /** * @var array $priority_egw2ical conversion of the priority egw => ical */ var $priority_egw2ical = array( 0 => 9, // low 1 => 5, // normal 2 => 3, // high 3 => 1, // urgent ); /** * @var array $priority_ical2egw conversion of the priority ical => egw */ var $priority_ical2egw = array( 9 => 0, 8 => 0, 7 => 0, // low 6 => 1, 5 => 1, 4 => 1, 0 => 1, // normal 3 => 2, 2 => 2, // high 1 => 3, // urgent ); /** * @var array $priority_egw2funambol conversion of the priority egw => funambol */ var $priority_egw2funambol = array( 0 => 0, // low 1 => 1, // normal 2 => 2, // high 3 => 2, // urgent ); /** * @var array $priority_funambol2egw conversion of the priority funambol => egw */ var $priority_funambol2egw = array( 0 => 0, // low 1 => 1, // normal 2 => 3, // high ); /** * manufacturer and name of the sync-client * * @var string */ var $productManufacturer = 'file'; var $productName = ''; /** * Shall we use the UID extensions of the description field? * * @var boolean */ var $uidExtension = false; /** * user preference: Use this timezone for import from and export to device * * @var string */ var $tzid = null; /** * Client CTCap Properties * * @var array */ var $clientProperties; /** * Entry callback * If set, this will be called on each discovered etry so it can be * modified. Entry is passed by reference, return true to keep the event * or false to skip it. * * @var callable */ var $entry_callback = null; /** * Set Logging * * @var boolean */ var $log = false; var $logfile="/tmp/log-infolog-vcal"; /** * Constructor * * @param array $_clientProperties client properties */ function __construct(&$_clientProperties = array()) { parent::__construct(); if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-infolog-vcal"; $this->clientProperties = $_clientProperties; } /** * Exports multiple InfoLogs * * @param array $tasks array of info_ids or task arrays * @param string $_version ='2.0' * @param string $_method =null only set for iTip messages * @param string $charset ='UTF-8' * @return string|boolean string with vCal or false on error (eg. no permission to read the event) */ function exportVCalendar(array $tasks, $_version='2.0', $_method=null, $charset='UTF-8') { $vcal = new Horde_Icalendar; foreach($tasks as $task) { if (!$this->exportVTODO($task, $_version, $_method, $charset, $vcal)) { return false; } } return $vcal->exportVCalendar(); } /** * Exports one InfoLog tast to an iCalendar VTODO * * @param int|array $task infolog_id or infolog-tasks data * @param string $_version ='2.0' could be '1.0' too * @param string $_method ='PUBLISH' * @param string $charset ='UTF-8' encoding of the vcalendar, default UTF-8 * @param Horde_Icalendar $vcal =null optional iCalendar object to add vtodo to * * @return string|boolean string with vCal or false on error (eg. no permission to read the event) */ function exportVTODO($task, $_version='2.0',$_method='PUBLISH', $charset='UTF-8',Horde_Icalendar $vcal=null) { if (is_array($task)) { $taskData = $task; } else { if (!($taskData = $this->read($task, true, 'server'))) return false; } 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 ($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 = Api\Translation::convert($taskData, Api\Translation::charset(), $charset); if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($taskData)."\n",3,$this->logfile); } if (!isset($vcal)) $vcal = new Horde_Icalendar; $vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware InfoLog '.$GLOBALS['egw_info']['apps']['infolog']['version'].'//'. strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']),array(),false); $vcal->setAttribute('VERSION',$_version,array(),false); if ($_method) $vcal->setAttribute('METHOD',$_method,array(),false); $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 (!calendar_timezones::add_vtimezone($vcal, $tzid)) { error_log(__METHOD__."() unknown TZID='$tzid', defaulting to user timezone '".Api\DateTime::$user_timezone->getName()."'!"); calendar_timezones::add_vtimezone($vcal, Api\DateTime::$user_timezone->getName()); $tzid = null; } if (!isset(self::$tz_cache[$tzid])) { self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); } } $vevent = Horde_Icalendar::newComponent('VTODO',$vcal); if (!isset($this->clientProperties['SUMMARY']['Size'])) { // 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'], 'X-INFOLOG-TYPE' => $taskData['info_type'], ) as $field => $value) { if (isset($this->clientProperties[$field]['Size'])) { $size = $this->clientProperties[$field]['Size']; $noTruncate = $this->clientProperties[$field]['NoTruncate']; if ($this->log && $size > 0) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . "() $field Size: $size, NoTruncate: " . ($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile); } //Horde::logMessage("VTODO $field Size: $size, NoTruncate: " . // ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); } else { $size = -1; $noTruncate = false; } $cursize = strlen($value); if (($size > 0) && $cursize > $size) { if ($noTruncate) { if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . "() $field omitted due to maximum size $size\n",3,$this->logfile); } //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); if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . "() $field truncated to maximum size $size\n",3,$this->logfile); } //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 ($_version == '1.0' && preg_match('/[^\x20-\x7F]/', $value)) { $options['CHARSET'] = $charset; switch ($this->productManufacturer) { case 'groupdav': if ($this->productName == 'kde') { $options['ENCODING'] = 'QUOTED-PRINTABLE'; } else { $options['CHARSET'] = ''; if (preg_match(Api\CalDAV\Handler::REQUIRE_QUOTED_PRINTABLE_ENCODING, $value)) { $options['ENCODING'] = 'QUOTED-PRINTABLE'; } else { $options['ENCODING'] = ''; } } break; case 'funambol': $options['ENCODING'] = 'FUNAMBOL-QP'; } } $vevent->setAttribute($field, $value, $options); } // only export startdate < duedate, as some clients (eg. CalDAV Sync for Android) have problems with that if ($taskData['info_startdate'] && (empty($taskData['info_enddate']) || $taskData['info_startdate'] <= $taskData['info_enddate'])) { self::setDateOrTime($vevent, 'DTSTART', $taskData['info_startdate'], $tzid); } if ($taskData['info_enddate']) { self::setDateOrTime($vevent, 'DUE', $taskData['info_enddate'], $tzid); } if ($taskData['info_datecompleted']) { self::setDateOrTime($vevent, 'COMPLETED', $taskData['info_datecompleted'], $tzid); } $vevent->setAttribute('DTSTAMP',time()); $vevent->setAttribute('CREATED', $taskData['info_created']); $vevent->setAttribute('LAST-MODIFIED', $taskData['info_datemodified']); $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 $vevent->setAttribute('X-INFOLOG-STATUS',$taskData['info_status']); $vevent->setAttribute('PERCENT-COMPLETE',$taskData['info_percent']); if ($this->productManufacturer == 'funambol' && (strpos($this->productName, 'outlook') !== false || strpos($this->productName, 'pocket pc') !== false)) { $priority = (int) $this->priority_egw2funambol[$taskData['info_priority']]; } else { $priority = (int) $this->priority_egw2ical[$taskData['info_priority']]; } $vevent->setAttribute('PRIORITY', $priority); // add ATTENDEE and ORGANIZER only if ATTENDEEs other then owner are specified if ($taskData['info_responsible'] && (count($taskData['info_responsible']) > 1 || $taskData['info_responsible'][0] != $taskData['info_onwer'])) { if (($url = $GLOBALS['egw']->accounts->id2name($taskData['info_owner'],'account_email'))) { $url = 'MAILTO:'.$url; } else { $url = 'urn:uuid:'.Api\CalDAV::generate_uid('accounts', $taskData['info_owner']); } $vevent->setAttribute('ORGANIZER',$url,array( 'CN' => $GLOBALS['egw']->accounts->id2name($taskData['info_owner'],'account_fullname'), 'X-EGROUPWARE-UID' => $taskData['info_owner'], ), true); foreach($taskData['info_responsible'] as $responsible) { if (($url = $GLOBALS['egw']->accounts->id2name($responsible,'account_email'))) { $url = 'MAILTO:'.$url; } else { $url = 'urn:uuid:'.Api\CalDAV::generate_uid('accounts', $responsible); } if ($responsible > 0) { $vevent->setAttribute('ATTENDEE',$url,array( 'CN' => $GLOBALS['egw']->accounts->id2name($responsible,'account_fullname'), 'CUTYPE' => 'INDIVIDUAL', 'X-EGROUPWARE-UID' => $responsible, ), true); } elseif ($responsible < 0) { $vevent->setAttribute('ATTENDEE',$url,array( 'CN' => $GLOBALS['egw']->accounts->id2name($responsible), 'CUTYPE' => 'GROUP', 'X-EGROUPWARE-UID' => $responsible, ), true); } } } // for CalDAV add all X-Properties previously parsed if ($this->productManufacturer == 'groupdav') { foreach($taskData as $name => $value) { if (substr($name, 0, 2) == '##') { if (($v = json_php_unserialize($value)) && is_array($v)) { $value = $v; } // fix certain stock fields like GEO, which are not in EGroupware schema, but Horde Icalendar requires a certain format switch($name) { case '##GEO': if (!is_array($value)) { if (strpos($value, ';') !== false) { list($lat, $long) = explode(';', $value); } else { list($long, $lat) = explode(',', $value); } $value = ['latitude' => $lat, 'logitude' => $long]; } break; } if ($name[2] == ':') { foreach((array)$value as $compvData) { $comp = Horde_Icalendar::newComponent(substr($name,3), $vevent); $comp->parsevCalendar($compvData,substr($name,3)); $vevent->addComponent($comp); } } elseif (is_array($value)) { $vevent->setAttribute(substr($name, 2), $value['value'], $value['params'], true, $value['values']); } else { $vevent->setAttribute(substr($name, 2), $value); } } } } $vcal->addComponent($vevent); $retval = $vcal->exportvCalendar(); if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($retval)."\n",3,$this->logfile); } // Horde::logMessage("exportVTODO:\n" . print_r($retval, true), // __FILE__, __LINE__, PEAR_LOG_DEBUG); return $retval; } /** * 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 $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, $time, $tzid) { $params = array(); //$time_in = $time; if ($tzid) { if (!isset(self::$tz_cache[$tzid])) { self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); } $tz = self::$tz_cache[$tzid]; } elseif(is_null($tzid)) { $tz = Api\DateTime::$user_timezone; } else { $tz = Api\DateTime::$server_timezone; } if (!is_a($time,'DateTime')) { $time = new Api\DateTime($time,Api\DateTime::$server_timezone); } $time->setTimezone($tz); // check for date --> export it as such if ($time->format('Hi') == '0000') { $arr = Api\DateTime::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 = Api\DateTime::to($time, 'ts'); } } //error_log(__METHOD__."(, '$attr', ".array2string($time_in).', '.array2string($tzid).') tz='.$tz->getName().', value='.array2string($value).(is_int($value)?date('Y-m-d H:i:s',$value):'')); $vevent->setAttribute($attr, $value, $params); } /** * Import a VTODO component of an iCal * * @param string $_vcalData * @param int $_taskID =-1 info_id, default -1 = new entry * @param boolean $merge =false merge data with existing entry (no longer used) * @param int $user =null delegate new task to this account_id, default null * @param string $charset =null The encoding charset for $text. Defaults to * utf-8 for new format, iso-8859-1 for old format. * @param string $caldav_name =null CalDAV URL name-part for new entries * @param array $callback_data =null array with callback and further parameters, first param is task to save * signature array callback($task, $param1, ...) * @return int|boolean integer info_id or false on error */ function importVTODO(&$_vcalData, $_taskID=-1, $merge=false, $user=null, $charset=null, $caldav_name=null, array $callback_data=null) { unset($merge); // no longer used, but required by function signature if ($this->tzid) { date_default_timezone_set($this->tzid); } $taskData = $this->vtodotoegw($_vcalData,$_taskID, $charset); if ($this->tzid) { date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); } if($this->entry_callback && is_callable($this->entry_callback)) { if(!call_user_func_array($this->entry_callback, array(&$taskData))) { // Callback cancelled entry return false; } } if (!$taskData) return false; // keep the dates $this->time2time($taskData, $this->tzid, false); if (empty($taskData['info_datecompleted'])) { $taskData['info_datecompleted'] = 0; } // setting owner or responsible for new tasks based on folder if (!is_null($user) && $_taskID <= 0) { if ($this->check_access($taskData, Acl::ADD)) { $taskData['info_owner'] = $user; } elseif (!in_array($user, (array)$taskData['info_responsible'])) { $taskData['info_responsible'][] = $user; } } if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($taskData)."\n",3,$this->logfile); } if ($caldav_name) { $taskData['caldav_name'] = $caldav_name; } // make sure not to empty fields not supported by iCal or not allowed to change by CalDAV if ($_taskID > 0 && ($old = $this->read($_taskID,true,'server'))) { // remove all values supported by iCal standard $old = array_diff_key($old, array_flip(array( 'info_subject','info_des','info_location','info_cat','info_responsible', 'info_startdate','info_enddate','info_priority','info_location', 'info_access','info_status','info_percent','info_datecompleted', ))); // remove all iCal fields not supported by EGroupware (stored like custom fields) foreach(array_keys($old) as $name) { if (substr($name,0,2) == '##') unset($old[$name]); } // merge in again all infolog fields not supported by iCal or not allowed to change $taskData = array_merge($taskData, $old); } if ($callback_data) { $callback = array_shift($callback_data); array_unshift($callback_data, $taskData); $taskData = call_user_func_array($callback, $callback_data); } return $this->write($taskData, true, true, false, false, false, 'ical'); } /** * 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 * @param string $charset The encoding charset for $text. Defaults to * utf-8 for new format, iso-8859-1 for old format. * * @return array of infolog_ids of matching entries */ function searchVTODO($_vcalData, $contentID=null, $relax=false, $charset=null) { $result = array(); if ($this->tzid) { date_default_timezone_set($this->tzid); } $taskData = $this->vtodotoegw($_vcalData, $contentID, $charset); if ($this->tzid) { date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); } if ($taskData) { if ($contentID) { $taskData['info_id'] = $contentID; } $result = $this->findInfo($taskData, $relax, $this->tzid); } return $result; } /** * Convert date(-array) to timestamp used in InfoLog * * @param int|array $date * @return int */ protected static function date2ts($date) { return is_scalar($date) ? $date : Api\DateTime::to($date, 'ts'); } /** * Convert VTODO into a eGW infolog entry * * @param string $_vcalData VTODO data * @param int $_taskID =-1 infolog_id of the entry * @param string $charset The encoding charset for $text. Defaults to * utf-8 for new format, iso-8859-1 for old format. * * @return array infolog entry or false on error */ function vtodotoegw($_vcalData, $_taskID=-1, $charset=null) { if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_taskID)\n" . array2string($_vcalData)."\n",3,$this->logfile); } $vcal = new Horde_Icalendar; if ($charset && $charset != 'utf-8') { $_vcalData = Api\Translation::convert($_vcalData, $charset, 'utf-8'); } if (!($vcal->parsevCalendar($_vcalData, 'VCALENDAR'))) { if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): No vCalendar Container found!\n",3,$this->logfile); } return false; } if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; } else { $minimum_uid_length = 8; } $taskData = false; foreach ($vcal->getComponents() as $component) { if (!is_a($component, 'Horde_Icalendar_Vtodo')) { if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): Not a vTODO container, skipping...\n",3,$this->logfile); } continue; } $taskData = array('info_cat' => $_taskID ? 0 : $GLOBALS['egw_info']['user']['preferences']['infolog']['cat_add_default']); if ($_taskID > 0) { $taskData['info_id'] = $_taskID; } // iOS reminder app only sets COMPLETED, but never STATUS nor PERCENT-COMPLETED // if we have no STATUS, set STATUS by existence of COMPLETED and/or PERCENT-COMPLETE and X-INFOLOG-STATUS // if we have no PERCENT-COMPLETE set it from STATUS: 0=NEEDS-ACTION, 10=IN-PROCESS, 100=COMPLETED try { $status = $component->getAttribute('STATUS'); } catch (Horde_Icalendar_Exception $e) { unset($e); $completed = $component->getAttributeDefault('COMPLETED', null); $x_infolog_status = $component->getAttributeDefault('X-INFOLOG-STATUS', null); // check if we have a X-INFOLOG-STATUS and it's completed state is different from given COMPLETED attr if (is_scalar($x_infolog_status) && ($this->_status2vtodo[$x_infolog_status] === 'COMPLETED') != is_scalar($completed)) { $percent_completed = $component->getAttributeDefault('PERCENT-COMPLETE', null); $status = $completed && is_scalar($completed) ? 'COMPLETED' : ($percent_completed && is_scalar($percent_completed) && $percent_completed > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION'); $component->setAttribute('STATUS', $status); if (!is_scalar($percent_completed)) { $component->setAttribute('PERCENT-COMPLETE', $percent_completed = $status == 'COMPLETED' ? 100 : ($status == 'NEEDS-ACTION' ? 0 : 10)); } if ($this->log) error_log(__METHOD__."() setting STATUS='$status' and PERCENT-COMPLETE=$percent_completed from COMPLETED and X-INFOLOG-STATUS='$x_infolog_status'\n",3,$this->logfile); } // new task without status --> set a default status of NEEDS-ACTION, as otherwise task is marked closed elseif($_taskID <= 0 && !is_scalar($x_infolog_status) && !is_scalar($completed)) { $component->setAttribute('STATUS', 'NEEDS-ACTION'); } else { if ($this->log) error_log(__METHOD__."() no STATUS, X-INFOLOG-STATUS='$x_infolog_status', COMPLETED".(is_scalar($completed)?'='.$completed:' not set')." --> leaving status and percent unchanged",3,$this->logfile); } } foreach ($component->getAllAttributes() as $attribute) { if (!$attribute['value'] && $attribute['value'] !== '0') continue; switch ($attribute['name']) { case 'CLASS': $taskData['info_access'] = strtolower($attribute['value']); break; case 'DESCRIPTION': $value = str_replace("\r\n", "\n", $attribute['value']); $matches = null; 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 'DURATION': if (!isset($taskData['info_startdate'])) { $taskData['info_startdate'] = $component->getAttributeDefault('DTSTART', null); } $attribute['value'] += $taskData['info_startdate']; $taskData['##DURATION'] = $attribute['value']; // fall throught case 'DUE': // even as EGroupware only displays the date, we can still store the full value // unless infolog get's stored, it does NOT truncate the time $taskData['info_enddate'] = self::date2ts($attribute['value']); break; case 'COMPLETED': $taskData['info_datecompleted'] = self::date2ts($attribute['value']); break; case 'DTSTART': $taskData['info_startdate'] = self::date2ts($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 'X-INFOLOG-STATUS': break; case 'STATUS': // check if we (still) have X-INFOLOG-STATUS set AND it would give an unchanged status (no change by the user) $taskData['info_status'] = $this->vtodo2status($attribute['value'], $component->getAttributeDefault('X-INFOLOG-STATUS', 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; case 'ATTENDEE': if (($uid = Api\CalDAV\Principals::url2uid($attribute['value'])) && is_numeric($uid)) { $taskData['info_responsible'][] = $uid; } break; case 'ORGANIZER': if (($uid = Api\CalDAV\Principals::url2uid($attribute['value'])) && is_numeric($uid)) { $taskData['info_owner'] = $uid; } break; case 'X-INFOLOG-TYPE': if (isset($this->enums['type'][$attribute['value']])) { $taskData['info_type'] = $attribute['value']; } break; // ignore all PROPS, we dont want to store like X-properties or unsupported props case 'DTSTAMP': case 'SEQUENCE': case 'CREATED': case 'LAST-MODIFIED': break; default: // X- attribute or other by EGroupware unsupported property //error_log(__METHOD__."() $attribute[name] = ".array2string($attribute)); // for attributes with multiple values in multiple lines, merge the values if (isset($taskData['##'.$attribute['name']])) { //error_log(__METHOD__."() taskData['##$attribute[name]'] = ".array2string($taskData['##'.$attribute['name']])); $attribute['values'] = array_merge( is_array($taskData['##'.$attribute['name']]) ? $taskData['##'.$attribute['name']]['values'] : (array)$taskData['##'.$attribute['name']], $attribute['values']); } $taskData['##'.$attribute['name']] = $attribute['params'] || count($attribute['values']) > 1 ? json_encode($attribute) : $attribute['value']; break; } } break; } // store included, but unsupported components like valarm as x-properties foreach($component->getComponents() as $comp) { $name = '##:'.strtoupper($comp->getType()); $compvData = $comp->exportvCalendar($comp,'utf-8'); if (isset($taskData[$name])) { $taskData[$name] = array($taskData[$name]); $taskData[$name][] = $compvData; } else { $taskData[$name] = $compvData; } } if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_taskID)\n" . ($taskData ? array2string($taskData) : 'FALSE') . "\n",3,$this->logfile); } return $taskData; } /** * Export an infolog entry as VNOTE * * @param int $_noteID the infolog_id of the entry * @param string $_type content type (e.g. text/plain) * @param string $charset ='UTF-8' encoding of the vcalendar, default UTF-8 * * @return string|boolean VNOTE representation of the infolog entry or false on error */ function exportVNOTE($_noteID, $_type, $charset='UTF-8') { if(!($note = $this->read($_noteID, true, 'server'))) return false; $note = Api\Translation::convert($note, Api\Translation::charset(), $charset); switch ($_type) { case 'text/plain': $txt = $note['info_subject']."\n\n".$note['info_des']; return $txt; case 'text/x-vnote': if (!empty($note['info_cat'])) { $cats = $this->get_categories(array($note['info_cat'])); $note['info_cat'] = Api\Translation::convert($cats[0], Api\Translation::charset(), $charset); } $vnote = new Horde_Icalendar_Vnote(); $vnote->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware InfoLog '.$GLOBALS['egw_info']['apps']['infolog']['version'].'//'. strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang'])); $vnote->setAttribute('VERSION', '1.1'); foreach (array( 'SUMMARY' => $note['info_subject'], 'BODY' => $note['info_des'], 'CATEGORIES' => $note['info_cat'], ) as $field => $value) { $options = array(); if (preg_match('/[^\x20-\x7F]/', $value)) { $options['CHARSET'] = $charset; switch ($this->productManufacturer) { case 'groupdav': if ($this->productName == 'kde') { $options['ENCODING'] = 'QUOTED-PRINTABLE'; } else { $options['CHARSET'] = ''; if (preg_match(Api\CalDAV\Handler::REQUIRE_QUOTED_PRINTABLE_ENCODING, $value)) { $options['ENCODING'] = 'QUOTED-PRINTABLE'; } else { $options['ENCODING'] = ''; } } break; case 'funambol': $options['ENCODING'] = 'FUNAMBOL-QP'; } } $vnote->setAttribute($field, $value, $options); } if ($note['info_startdate']) { $vnote->setAttribute('CREATED',$note['info_startdate']); } else { $vnote->setAttribute('CREATED',$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'); $retval = $vnote->exportvCalendar(); if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($retval)."\n",3,$this->logfile); } return $retval; } return false; } /** * Import a VNOTE component of an iCal * * @param string $_vcalData * @param string $_type content type (eg.g text/plain) * @param int $_noteID =-1 info_id, default -1 = new entry * @param boolean $merge =false merge data with existing entry (no longer used) * @param string $charset The encoding charset for $text. Defaults to * utf-8 for new format, iso-8859-1 for old format. * * @return int|boolean integer info_id or false on error */ function importVNOTE(&$_vcalData, $_type, $_noteID=-1, $merge=false, $charset=null) { unset($merge); // no longer used, but required by function signature if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($_vcalData)."\n",3,$this->logfile); } if (!($note = $this->vnotetoegw($_vcalData, $_type, $_noteID, $charset))) return false; if($_noteID > 0) $note['info_id'] = $_noteID; if (empty($note['info_status'])) $note['info_status'] = 'done'; if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . array2string($note)."\n",3,$this->logfile); } return $this->write($note, true, true, false); } /** * Search a matching infolog entry for the VNOTE data * * @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 * @param string $charset The encoding charset for $text. Defaults to * utf-8 for new format, iso-8859-1 for old format. * * @return infolog_id of a matching entry or false, if nothing was found */ function searchVNOTE($_vcalData, $_type, $contentID=null, $relax=false, $charset=null) { if (!($note = $this->vnotetoegw($_vcalData, $_type, $contentID, $charset))) return array(); if ($contentID) $note['info_id'] = $contentID; unset($note['info_startdate']); return $this->findInfo($note, $relax, $this->tzid); } /** * Convert VTODO into a eGW infolog entry * * @param string $_data VNOTE data * @param string $_type content type (eg.g text/plain) * @param int $_noteID =-1 infolog_id of the entry * @param string $charset The encoding charset for $text. Defaults to * utf-8 for new format, iso-8859-1 for old format. * * @return array infolog entry or false on error */ function vnotetoegw($_data, $_type, $_noteID=-1, $charset=null) { if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_type, $_noteID)\n" . array2string($_data)."\n",3,$this->logfile); } $note = false; switch ($_type) { case 'text/plain': $note = array(); $note['info_type'] = 'note'; $txt = str_replace("\r\n", "\n", Api\Translation::convert($_data, $charset)); $match = null; if (preg_match('/([^\n]+)\n\n(.*)/ms', $txt, $match)) { $note['info_subject'] = $match[1]; $note['info_des'] = $match[2]; } else { $note['info_subject'] = $txt; } break; case 'text/x-vnote': $vnote = new Horde_Icalendar; if ($charset && $charset != 'utf-8') { $_data = Api\Translation::convert($_data, $charset, 'utf-8'); } if (!$vnote->parsevCalendar($_data, 'VCALENDAR')) return false; $components = $vnote->getComponent(); foreach ($components as $component) { if (is_a($component, 'Horde_Icalendar_Vnote')) { $note = array(); $note['info_type'] = 'note'; foreach ($component->getAllAttributes() as $attribute) { switch ($attribute['name']) { case 'BODY': $note['info_des'] = str_replace("\r\n", "\n", $attribute['value']); break; case 'SUMMARY': $note['info_subject'] = str_replace("\r\n", "\n", $attribute['value']); break; case 'CATEGORIES': if ($attribute['value']) { $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_noteID); $note['info_cat'] = $cats[0]; } break; } } } } } if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_type, $_noteID)\n" . ($note ? array2string($note) : 'FALSE') ."\n",3,$this->logfile); } return $note; } /** * Set the supported fields * * Currently we only store manufacturer and name * * @param string $_productManufacturer * @param string $_productName */ function setSupportedFields($_productManufacturer='', $_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; } 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']; } } } if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. '(' . $this->productManufacturer . ', '. $this->productName .', ' . ($this->tzid ? $this->tzid : Api\DateTime::$user_timezone->getName()) . ")\n" , 3, $this->logfile); } //Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' // . $this->productName .', ' . // ($this->tzid ? $this->tzid : Api\DateTime::$user_timezone->getName()) .')', // __FILE__, __LINE__, PEAR_LOG_DEBUG); } }