egroupware/infolog/inc/class.infolog_sif.inc.php
Jörg Lehrke b6097fa156 SyncML Content Handling
* 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)
2010-02-09 21:56:39 +00:00

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];
}
}
}
}