* @author Joerg Lehrke * @package infolog * @subpackage syncml * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ require_once EGW_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 = ''; const SIF_decl = '1.1'; /** * name and version of the sync-client * * @var string */ var $productName = 'mozilla plugin'; var $productSoftwareVersion = '0.3'; /** * Shall we use the UID extensions of the description field? * * @var boolean */ var $uidExtension = false; /** * 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 * */ 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]); $value = $time->format('Y-m-d'); } else { $time->setTimezone(self::$tz_cache[$tzid]); $value = $time->format('Ymd\THis'); } return $value; } 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" . 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 .= "0"; continue; } $sifTask .= "1"; case 'DueDate': case 'StartDate': $sifTask .= "<$sifField>"; if (!empty($value)) { $sifTask .= $this->getDateTime($value, $this->tzid); } $sifTask .= ""; break; case 'Importance': if ($value > 3) $value = 3; $sifTask .= "<$sifField>$value"; break; case 'Sensitivity': $value = ($value == 'private' ? '2' : '0'); $sifTask .= "<$sifField>$value"; 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"; 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_NOQUOTES, 'utf-8'); $sifTask .= "<$sifField>$value"; break; } } $sifTask .= '00'; return $sifTask; case 'note': $sifNote = self::xml_decl . "\n" . 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 .= ''; 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"; break; } } $sifNote .= ''; 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]; } } } }