egroupware_official/infolog/inc/class.infolog_ical.inc.php

1270 lines
37 KiB
PHP

<?php
/**
* EGroupware - InfoLog - iCalendar Parser
*
* @link http://www.egroupware.org
* @author Lars Kneschke <lkneschke@egroupware.org>
* @author Joerg Lehrke <jlehrke@noc.de>
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @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);
}
}