mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-27 10:23:28 +01:00
b6097fa156
* Improved find-methods * Timezone support for InfoLog * SyncML Preferences - addressbook and address list are now joined - Primary User Group for addressbook and calendar * SlowSync uses old mapping information (can be disabled within the preferences)
667 lines
17 KiB
PHP
667 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* InfoLog - SIF Parser
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @author Lars Kneschke <lkneschke@egroupware.org>
|
|
* @author Joerg Lehrke <jlehrke@noc.de>
|
|
* @package infolog
|
|
* @subpackage syncml
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @version $Id$
|
|
*/
|
|
|
|
require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php';
|
|
|
|
/**
|
|
* InfoLog: Create and parse SIF
|
|
*
|
|
*/
|
|
class infolog_sif extends infolog_bo
|
|
{
|
|
// array containing the result of the xml parser
|
|
var $_extractedSIFData;
|
|
|
|
// array containing the current mappings(task or note)
|
|
var $_currentSIFMapping;
|
|
|
|
var $_sifNoteMapping = array(
|
|
'Body' => 'info_des',
|
|
'Categories' => 'info_cat',
|
|
'Color' => '',
|
|
'Date' => 'info_startdate',
|
|
'Height' => '',
|
|
'Left' => '',
|
|
'Subject' => 'info_subject',
|
|
'Top' => '',
|
|
'Width' => '',
|
|
);
|
|
|
|
// mappings for SIFTask to InfologTask
|
|
var $_sifTaskMapping = array(
|
|
'ActualWork' => '',
|
|
'BillingInformation' => '',
|
|
'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 = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
const SIF_decl = '<SIFVersion>1.1</SIFVersion>';
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* user preference: Use this timezone for import from and export to device
|
|
*
|
|
* @var string
|
|
*/
|
|
var $tzid = null;
|
|
|
|
/**
|
|
* Set Logging
|
|
*
|
|
* @var boolean
|
|
*/
|
|
var $log = false;
|
|
var $logfile="/tmp/log-infolog-sif";
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
*/
|
|
function __construct()
|
|
{
|
|
parent::__construct();
|
|
if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-infolog-sif";
|
|
$this->vCalendar = new Horde_iCalendar;
|
|
}
|
|
|
|
/**
|
|
* Get DateTime value for a given time and timezone
|
|
*
|
|
* @param int|string|DateTime $time in server-time as returned by calendar_bo for $data_format='server'
|
|
* @param string $tzid TZID of event or 'UTC' or NULL for palmos timestamps in usertime
|
|
* @return mixed attribute value to set: integer timestamp if $tzid == 'UTC' otherwise Ymd\THis string IN $tzid
|
|
*/
|
|
function getDateTime($time, $tzid)
|
|
{
|
|
if (empty($tzid) || $tzid == 'UTC')
|
|
{
|
|
return $this->vCalendar->_exportDateTime(egw_time::to($time,'ts'));
|
|
}
|
|
if (!is_a($time,'DateTime'))
|
|
{
|
|
$time = new egw_time($time,egw_time::$server_timezone);
|
|
}
|
|
if (!isset(self::$tz_cache[$tzid]))
|
|
{
|
|
self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
|
|
}
|
|
// check for date --> export it as such
|
|
if ($time->format('Hi') == '0000')
|
|
{
|
|
$arr = egw_time::to($time, 'array');
|
|
$time = new egw_time($arr, self::$tz_cache[$tzid]);
|
|
}
|
|
else
|
|
{
|
|
$time->setTimezone(self::$tz_cache[$tzid]);
|
|
}
|
|
return $time->format('Ymd\THis');
|
|
}
|
|
|
|
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]))
|
|
{
|
|
$this->_extractedSIFData[$this->_currentSIFMapping[$_tag]] = trim($this->sifData);
|
|
}
|
|
unset($this->sifData);
|
|
}
|
|
|
|
function characterData($_parser, $_data)
|
|
{
|
|
$this->sifData .= $_data;
|
|
}
|
|
|
|
/**
|
|
* Convert SIF data into a eGW infolog entry
|
|
*
|
|
* @param string $sifData the SIF data
|
|
* @param string $_sifType type (note/task)
|
|
* @param int $_id=-1 the infolog id
|
|
* @return array infolog entry or false on error
|
|
*/
|
|
function siftoegw($sifData, $_sifType, $_id=-1)
|
|
{
|
|
|
|
if ($this->log)
|
|
{
|
|
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_sifType, $_id)\n" .
|
|
array2string($sifData) . "\n", 3, $this->logfile);
|
|
}
|
|
|
|
$sysCharSet = $GLOBALS['egw']->translation->charset();
|
|
|
|
switch ($_sifType)
|
|
{
|
|
case 'note':
|
|
$this->_currentSIFMapping = $this->_sifNoteMapping;
|
|
break;
|
|
|
|
case 'task':
|
|
$this->_currentSIFMapping = $this->_sifTaskMapping;
|
|
break;
|
|
|
|
default:
|
|
// we don't know how to handle this
|
|
return false;
|
|
}
|
|
|
|
$this->xml_parser = xml_parser_create('UTF-8');
|
|
xml_set_object($this->xml_parser, $this);
|
|
xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, false);
|
|
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)
|
|
{
|
|
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))
|
|
{
|
|
if ($this->log)
|
|
{
|
|
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[PARSER FAILD]\n",
|
|
3, $this->logfile);
|
|
}
|
|
return false;
|
|
}
|
|
$infoData = array();
|
|
|
|
switch ($_sifType)
|
|
{
|
|
case 'task':
|
|
$infoData['info_type'] = 'task';
|
|
$infoData['info_status'] = 'not-started';
|
|
|
|
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");
|
|
if (empty($value)) continue;
|
|
|
|
switch($key)
|
|
{
|
|
case 'info_access':
|
|
$infoData[$key] = ((int)$value > 0) ? 'private' : 'public';
|
|
break;
|
|
|
|
case 'info_datecompleted':
|
|
case 'info_enddate':
|
|
case 'info_startdate':
|
|
if (!empty($value))
|
|
{
|
|
$infoData[$key] = $this->vCalendar->_parseDateTime($value);
|
|
// somehow the client always deliver a timestamp about 3538 seconds, when no startdate set.
|
|
if ($infoData[$key] < 10000) unset($infoData[$key]);
|
|
}
|
|
break;
|
|
|
|
|
|
case 'info_cat':
|
|
if (!empty($value))
|
|
{
|
|
$categories = $this->find_or_add_categories(explode(';', $value), $_id);
|
|
$infoData['info_cat'] = $categories[0];
|
|
}
|
|
break;
|
|
|
|
case 'info_priority':
|
|
$infoData[$key] = (int)$value;
|
|
break;
|
|
|
|
case 'info_status':
|
|
switch ($value)
|
|
{
|
|
case '0':
|
|
$infoData[$key] = 'not-started';
|
|
break;
|
|
case '1':
|
|
$infoData[$key] = 'ongoing';
|
|
break;
|
|
case '2':
|
|
$infoData[$key] = 'done';
|
|
$infoData['info_percent'] = 100;
|
|
break;
|
|
case '3':
|
|
$infoData[$key] = 'waiting';
|
|
break;
|
|
case '4':
|
|
if ($this->productName == 'blackberry plug-in')
|
|
{
|
|
$infoData[$key] = 'deferred';
|
|
}
|
|
else
|
|
{
|
|
$infoData[$key] = 'cancelled';
|
|
}
|
|
break;
|
|
default:
|
|
$infoData[$key] = 'ongoing';
|
|
}
|
|
break;
|
|
|
|
case 'complete':
|
|
$infoData['info_status'] = 'done';
|
|
$infoData['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)
|
|
{
|
|
$infoData['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)
|
|
{
|
|
$infoData['info_id_parent'] = $this->getParentID($matches[1]);
|
|
}
|
|
//$value = str_replace($matches[0], '', $value);
|
|
}
|
|
|
|
default:
|
|
$infoData[$key] = str_replace("\r\n", "\n", $value);
|
|
}
|
|
if ($this->log)
|
|
{
|
|
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
|
|
"key=$key => value=" . $infoData[$key] . "\n", 3, $this->logfile);
|
|
}
|
|
}
|
|
if (empty($infoData['info_datecompleted']))
|
|
{
|
|
$infoData['info_datecompleted'] = 0;
|
|
}
|
|
break;
|
|
|
|
case 'note':
|
|
$infoData['info_type'] = 'note';
|
|
|
|
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);
|
|
switch ($key)
|
|
{
|
|
case 'info_startdate':
|
|
if (!empty($value))
|
|
{
|
|
$infoData[$key] = $this->vCalendar->_parseDateTime($value);
|
|
// somehow the client always deliver a timestamp about 3538 seconds, when no startdate set.
|
|
if ($infoData[$key] < 10000) $infoData[$key] = '';
|
|
}
|
|
else
|
|
{
|
|
$infoData[$key] = '';
|
|
}
|
|
break;
|
|
|
|
case 'info_cat':
|
|
if (!empty($value))
|
|
{
|
|
$categories = $this->find_or_add_categories(explode(';', $value), $_id);
|
|
$infoData['info_cat'] = $categories[0];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
$infoData[$key] = str_replace("\r\n", "\n", $value);
|
|
}
|
|
if ($this->log)
|
|
{
|
|
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
|
|
"key=$key => value=" . $infoData[$key] . "\n", 3, $this->logfile);
|
|
}
|
|
}
|
|
}
|
|
if ($this->log)
|
|
{
|
|
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
|
|
array2string($infoData) . "\n", 3, $this->logfile);
|
|
}
|
|
return $infoData;
|
|
}
|
|
|
|
/**
|
|
* 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, $contentID))) return array();
|
|
|
|
if ($contentID) $egwData['info_id'] = $contentID;
|
|
|
|
if ($_sifType == 'note') unset($egwData['info_startdate']);
|
|
|
|
return $this->findInfo($egwData, $relax, $this->tzid);
|
|
}
|
|
|
|
/**
|
|
* 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 ($this->tzid)
|
|
{
|
|
date_default_timezone_set($this->tzid);
|
|
}
|
|
$egwData = $this->siftoegw($_sifData, $_sifType, $_id);
|
|
if ($this->tzid)
|
|
{
|
|
date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
|
|
}
|
|
if (!$egwData) return false;
|
|
|
|
if ($_id > 0) $egwData['info_id'] = $_id;
|
|
|
|
return $this->write($egwData, true, true, false);
|
|
}
|
|
|
|
|
|
/**
|
|
* 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();
|
|
|
|
if (!($infoData = $this->read($_id, true, 'server'))) return false;
|
|
|
|
switch($_sifType)
|
|
{
|
|
case 'task':
|
|
if ($infoData['info_id_parent'])
|
|
{
|
|
$parent = $this->read($infoData['info_id_parent']);
|
|
$infoData['info_id_parent'] = $parent['info_uid'];
|
|
}
|
|
else
|
|
{
|
|
$infoData['info_id_parent'] = '';
|
|
}
|
|
|
|
if (!preg_match('/\[UID:.+\]/m', $infoData['info_des']))
|
|
{
|
|
$infoData['info_des'] .= "\r\n[UID:" . $infoData['info_uid'] . "]";
|
|
if ($infoData['info_id_parent'] != '')
|
|
{
|
|
$infoData['info_des'] .= "\r\n[PARENT_UID:" . $infoData['info_id_parent'] . "]";
|
|
}
|
|
}
|
|
|
|
$sifTask = self::xml_decl . "\n<task>" . self::SIF_decl;
|
|
|
|
foreach ($this->_sifTaskMapping as $sifField => $egwField)
|
|
{
|
|
if (empty($egwField)) continue;
|
|
|
|
$value = $GLOBALS['egw']->translation->convert($infoData[$egwField], $sysCharSet, 'utf-8');
|
|
|
|
switch ($sifField)
|
|
{
|
|
|
|
case 'Complete':
|
|
// is handled with DateCompleted
|
|
break;
|
|
|
|
case 'DateCompleted':
|
|
if ($infoData[info_status] != 'done')
|
|
{
|
|
$sifTask .= "<DateCompleted></DateCompleted><Complete>0</Complete>";
|
|
continue;
|
|
}
|
|
$sifTask .= "<Complete>1</Complete>";
|
|
|
|
case 'DueDate':
|
|
case 'StartDate':
|
|
$sifTask .= '<$sifField>';
|
|
if (!empty($value))
|
|
{
|
|
$sifTask .= $this->getDateTime($value, $this->tzid);
|
|
}
|
|
$sifTask .= '</$sifField>';
|
|
break;
|
|
|
|
case 'Importance':
|
|
if ($value > 3) $value = 3;
|
|
$sifTask .= "<$sifField>$value</$sifField>";
|
|
break;
|
|
|
|
case 'Sensitivity':
|
|
$value = ($value == 'private' ? '2' : '0');
|
|
$sifTask .= "<$sifField>$value</$sifField>";
|
|
break;
|
|
|
|
case 'Status':
|
|
switch ($value)
|
|
{
|
|
case 'cancelled':
|
|
case 'deferred':
|
|
$value = '4';
|
|
break;
|
|
case 'waiting':
|
|
case 'nonactive':
|
|
$value = '3';
|
|
break;
|
|
case 'done':
|
|
case 'archive':
|
|
case 'billed':
|
|
$value = '2';
|
|
break;
|
|
case 'not-started':
|
|
case 'template':
|
|
$value = '0';
|
|
break;
|
|
default: //ongoing
|
|
$value = 1;
|
|
break;
|
|
}
|
|
$sifTask .= "<$sifField>$value</$sifField>";
|
|
break;
|
|
|
|
case 'Categories':
|
|
if (!empty($value) && $value)
|
|
{
|
|
$value = implode('; ', $this->get_categories(array($value)));
|
|
$value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8');
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
default:
|
|
$value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8');
|
|
$sifTask .= "<$sifField>$value</$sifField>";
|
|
break;
|
|
}
|
|
}
|
|
$sifTask .= '<ActualWork>0</ActualWork><IsRecurring>0</IsRecurring></task>';
|
|
return $sifTask;
|
|
|
|
case 'note':
|
|
$sifNote = self::xml_decl . "\n<note>" . self::SIF_decl;
|
|
|
|
foreach ($this->_sifNoteMapping as $sifField => $egwField)
|
|
{
|
|
if(empty($egwField)) continue;
|
|
|
|
$value = $GLOBALS['egw']->translation->convert($infoData[$egwField], $sysCharSet, 'utf-8');
|
|
|
|
switch ($sifField)
|
|
{
|
|
case 'Date':
|
|
$sifNote .= '<$sifField>';
|
|
if (!empty($value))
|
|
{
|
|
$sifNote .= $this->getDateTime($value, $this->tzid);
|
|
}
|
|
$sifNote .= '</$sifField>';
|
|
break;
|
|
|
|
case 'Categories':
|
|
if (!empty($value))
|
|
{
|
|
$value = implode('; ', $this->get_categories(array($value)));
|
|
$value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8');
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
default:
|
|
$value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8');
|
|
$sifNote .= "<$sifField>$value</$sifField>";
|
|
break;
|
|
}
|
|
}
|
|
$sifNote .= '</note>';
|
|
return $sifNote;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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='')
|
|
{
|
|
$state = &$_SESSION['SyncML.state'];
|
|
$deviceInfo = $state->getClientDeviceInfo();
|
|
|
|
if (isset($deviceInfo) && is_array($deviceInfo))
|
|
{
|
|
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'];
|
|
}
|
|
}
|
|
}
|
|
// 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];
|
|
}
|
|
}
|
|
}
|
|
}
|