From 1ad5811345e1a177f45e49ed2b3554fd6636834c Mon Sep 17 00:00:00 2001 From: Lars Kneschke Date: Thu, 17 Aug 2006 03:16:09 +0000 Subject: [PATCH] SyncML fixes - we dont handle tasks in calendar that way --- phpgwapi/inc/horde/Horde/SyncML/State.php | 937 ++++++++++++++++++++++ phpgwapi/inc/horde/XML/WBXML/Decoder.php | 668 +++++++++++++++ 2 files changed, 1605 insertions(+) create mode 100644 phpgwapi/inc/horde/Horde/SyncML/State.php create mode 100644 phpgwapi/inc/horde/XML/WBXML/Decoder.php diff --git a/phpgwapi/inc/horde/Horde/SyncML/State.php b/phpgwapi/inc/horde/Horde/SyncML/State.php new file mode 100644 index 0000000000..c503fdcbfd --- /dev/null +++ b/phpgwapi/inc/horde/Horde/SyncML/State.php @@ -0,0 +1,937 @@ + + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Anthony Mills + * @version $Revision$ + * @since Horde 3.0 + * @package Horde_SyncML + */ +class Horde_SyncML_State { + + var $_sessionID; + + var $_verProto; + + var $_msgID; + + var $_targetURI; + + var $_sourceURI; + + var $_version; + + var $_locName; + + var $_password; + + var $_isAuthorized; + + var $_uri; + + var $_uriMeta; + + var $_syncs = array(); + + var $_clientAnchorNext = array(); // written to db after successful sync + + var $_serverAnchorLast = array(); + + var $_serverAnchorNext = array(); // written to db after successful sync + + var $_clientDeviceInfo = array(); + + // array list of changed items, which need to be synced to the client + var $_changedItems; + + // array list of deleted items, which need to be synced to the client + var $_deletedItems; + + // array list of added items, which need to be synced to the client + var $_addedItems; + + // bool flag that we need to more data + var $_syncStatus; + + var $_log = array(); + + // stores if we received Alert 222 already + var $_receivedAlert222 = false; + + /** + * Creates a new instance of Horde_SyncML_State. + */ + function Horde_SyncML_State($sourceURI, $locName, $sessionID, $password = false) + { + $this->setSourceURI($sourceURI); + $this->setLocName($locName); + $this->setSessionID($sessionID); + if ($password) { + $this->setPassword($password); + } + + $this->isAuthorized = false; + } + + /** + * Returns the DataTree used as persistence layer for SyncML. The + * datatree var should not be a class member of State as State is + * stored as a session var. Resource handles (=db connections) + * cannot be stored in sessions. + * + * @return object DataTree The DataTree object. + */ + function &getDataTree() + { + $driver = $GLOBALS['conf']['datatree']['driver']; + $params = Horde::getDriverConfig('datatree', $driver); + $params = array_merge($params, array( 'group' => 'syncml' )); + + return DataTree::singleton($driver, $params); + } + + function getLocName() + { + if(isset($this->_locName)) + return $this->_locName; + else + return False; + } + + function getSourceURI() + { + return $this->_sourceURI; + } + + function getTargetURI() + { + return $this->_targetURI; + } + + function getVersion() + { + return $this->_version; + } + + function &getAddedItems($_type) + { + if(isset($this->_addedItems[$_type])) + { + return $this->_addedItems[$_type]; + } + + return false; + } + + function &getChangedItems($_type) + { + if(isset($this->_changedItems[$_type])) + { + return $this->_changedItems[$_type]; + } + + return false; + } + + function &getDeletedItems($_type) + { + if(isset($this->_deletedItems[$_type])) + { + return $this->_deletedItems[$_type]; + } + + return false; + } + + function getMoreDataPending() + { + return $this->_moreDataPending; + } + + function getMsgID() + { + return $this->_msgID; + } + + function setWBXML($wbxml) + { + $this->_wbxml = $wbxml; + } + + function isWBXML() + { + return !empty($this->_wbxml); + } + + function &getSyncStatus() + { + return $this->_syncStatus; + } + + function setAddedItems($_type, $_addedItems) + { + $this->_addedItems[$_type] = $_addedItems; + } + + function setChangedItems($_type, $_changedItems) + { + $this->_changedItems[$_type] = $_changedItems; + } + + function setClientDeviceInfo($clientDeviceInfo) + { + $this->_clientDeviceInfo = $clientDeviceInfo; + } + + function setDeletedItems($_type, $_deletedItems) + { + $this->_deletedItems[$_type] = $_deletedItems; + } + + function setMoreDataPending($_state) + { + $this->_moreDataPending = $_state; + } + + /** + * Setter for property msgID. + * @param msgID New value of property msgID. + */ + function setMsgID($msgID) + { + $this->_msgID = $msgID; + } + + /** + * Setter for property locName. + * @param locName New value of property locName. + */ + function setLocName($locName) + { + $this->_locName = $locName; + } + + /** + * Setter for property locName. + * @param locName New value of property locName. + */ + function setPassword($password) + { + $this->_password = $password; + } + + function setSourceURI($sourceURI) + { + $this->_sourceURI = $sourceURI; + } + + function setSyncStatus($_syncStatus) + { + #Horde::logMessage('SyncML: syncState set to ==> ' . $_syncStatus, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $this->_syncStatus = $_syncStatus; + } + + function setTargetURI($targetURI) + { + $this->_targetURI = $targetURI; + } + + function setVersion($version) + { + $this->_version = $version; + + if ($version == 0) { + $this->_uri = NAME_SPACE_URI_SYNCML; + $this->_uriMeta = NAME_SPACE_URI_METINF; + $this->_uriDevInf = NAME_SPACE_URI_DEVINF; + } else { + $this->_uri = NAME_SPACE_URI_SYNCML_1_1; + $this->_uriMeta = NAME_SPACE_URI_METINF_1_1; + $this->_uriDevInf = NAME_SPACE_URI_DEVINF_1_1; + } + } + + function setSessionID($sessionID) + { + $this->_sessionID = $sessionID; + } + + function isAuthorized() + { + if (!$this->_isAuthorized) { + + if(strstr($this->_locName,'@') === False) + { + $this->_locName .= '@'.$GLOBALS['phpgw_info']['server']['default_domain']; + } + + #Horde::logMessage('SyncML: Authenticate ' . $this->_locName . ' - ' . $this->_password, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + if($GLOBALS['sessionid'] = $GLOBALS['phpgw']->session->create($this->_locName,$this->_password,'text','u')) + { + $this->_isAuthorized = true; + #Horde::logMessage('SyncML_EGW: Authentication of ' . $this->_locName . '/' . $GLOBALS['sessionid'] . ' succeded' , __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + else + { + $this->_isAuthorized = false; + Horde::logMessage('SyncML: Authentication of ' . $this->_locName . ' failed' , __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + } + else + { + // store sessionID in a variable, because ->verify maybe resets that value + $sessionID = session_id(); + if(!$GLOBALS['phpgw']->session->verify($sessionID, 'staticsyncmlkp3')) + Horde::logMessage('SyncML_EGW: egw session('.$sessionID. ') not verified ' , __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + + return $this->_isAuthorized; + } + + function clearSync($target) + { + unset($this->_syncs[$target]); + } + + function setSync($target, $sync) + { + $this->_syncs[$target] = $sync; + } + + function getSync($target) + { + if (isset($this->_syncs[$target])) { + return $this->_syncs[$target]; + } else { + return false; + } + } + + function getTargets() + { + if(count($this->_syncs) < 1) + return FALSE; + + foreach($this->_syncs as $target => $sync) + { + $targets[] = $target; + } + + return $targets; + } + + function getURI() + { + /* + * The non WBXML devices (notably P900 and Sync4j seem to get confused + * by a element. They require + * just . So don't use an ns for non wbxml devices. + */ + if ($this->isWBXML()) { + return $this->_uri; + } else { + return ''; + } + } + function getURIMeta() + { + return $this->_uriMeta; + } + + function getURIDevInf() + { + return $this->_uriDevInf; + } + + + /** + * Converts a Horde GUID (like + * kronolith:0d1b415fc124d3427722e95f0e926b75) to a client ID as + * used by the sync client (like 12) returns false if no such id + * is stored yet. + * + * Remember that the datatree is really a tree disguised as a + * table. So to look up the guid above, getId first looks for an + * entry 'kronolith' and then for an entry + * 0d1b415fc124d3427722e95f0e926b75 with kronolith as parent. + */ + function getLocID($type, $guid) + { + $dt = &$this->getDataTree(); + $id = $dt->getId($this->_locName . $this->_sourceURI . $type . $guid); + if (is_a($id, 'PEAR_Error')) { + return false; + } + + $gid = $dt->getObjectById($id); + if (is_a($gid, 'PEAR_Error')) { + return false; + } + + return $gid->get('locid'); + } + + /** + * Puts a given client $locid and Horde server $guid pair into the + * map table to allow mapping between the client's and server's + * IDs. Actually there are two maps: from the localid to the guid + * and vice versa. The localid is converted to a key as follows: + * this->_locName . $this->_sourceURI . $type . $locid so you can + * have different syncs with different devices. If an entry + * already exists, it is overwritten. + */ + function setUID($type, $locid, $guid, $ts=0) + { + $dt = &$this->getDataTree(); + + // Set $locid. + $gid = &new DataTreeObject($this->_locName . $this->_sourceURI . $type . $guid); + $gid->set('type', $type); + $gid->set('locid', $locid); + $gid->set('ts', $ts); + + $r = $dt->add($gid); + if (is_a($r, 'PEAR_Error')) { + // Object already exists: update instead. + $r = $dt->updateData($gid); + } + $this->dieOnError($r, __FILE__, __LINE__); + + // Set $globaluid + $lid = &new DataTreeObject($this->_locName . $this->_sourceURI . $type . $locid); + $lid->set('globaluid', $guid); + $r = $dt->add($lid); + if (is_a($r, 'PEAR_Error')) { + // object already exists: update instead. + $r = $dt->updateData($lid); + } + $this->dieOnError($r, __FILE__, __LINE__); + } + + /** + * Retrieves the Horde server guid (like + * kronolith:0d1b415fc124d3427722e95f0e926b75) for a given client + * locid. Returns false if no such id is stored yet. + * + * Opposite of getLocId which returns the locid for a given guid. + */ + function getGlobalUID($type, $locid) + { + $this->dieOnError($type, __FILE__, __LINE__); + $this->dieOnError($locid, __FILE__, __LINE__); + $this->dieOnError($locid, __FILE__, __LINE__); + $this->dieOnError($this->_locName, __FILE__, __LINE__); + $this->dieOnError($this->_sourceURI, __FILE__, __LINE__); + + $dt = &$this->getDataTree(); + + $id = $dt->getId($this->_locName . $this->_sourceURI . $type . $locid); + if (is_a($id, 'PEAR_Error')) { + return false; + } + $lid = $dt->getObjectById($id); + if (is_a($lid, 'PEAR_Error')) { + return false; + } + + return $lid->get('globaluid'); + } + + /** + * Returns the timestamp (if set) of the last change to the + * obj:guid, that was caused by the client. This is stored to + * avoid mirroring these changes back to the client. + */ + function getChangeTS($type, $guid) + { + $dt = &$this->getDataTree(); + + $id = $dt->getId($this->_locName . $this->_sourceURI . $type . $guid); + if (is_a($id, 'PEAR_Error')) { + return false; + } + + $gid = $dt->getObjectById($id); + if (is_a($gid, 'PEAR_Error')) { + return false; + } + + return $gid->get('ts'); + } + + /** + * Removes the locid<->guid mapping for the given locid. Returns + * the guid that was removed or false if no mapping entry was + * found. + */ + function removeUID($type, $locid) + { + $dt = &$this->getDataTree(); + + $id = $dt->getId($this->_locName . $this->_sourceURI . $type . $locid); + if (is_a($id, 'PEAR_Error')) { + Horde::logMessage("SyncML: state->removeUID(type=$type,locid=$locid) : nothing to remove", __FILE__, __LINE__, PEAR_LOG_DEBUG); + return false; + } + $lid = $dt->getObjectById($id); + $guid = $lid->get('globaluid'); + Horde::logMessage("SyncML: state->removeUID(type=$type,locid=$locid) : removing guid:$guid and lid:$lid", __FILE__, __LINE__, PEAR_LOG_DEBUG); + $dt->remove($guid); + $dt->remove($lid); + + return $guid; + } + + /** + * This function should use DevINF information. + */ + function getPreferedContentType($type) + { +# if ($type == 'contacts') { +# return 'text/x-vcard'; +# } elseif ($type == 'notes') { +# return 'text/x-vnote'; +# } elseif ($type == 'tasks') { +# return 'text/x-vcalendar'; +# } elseif ($type == 'calendar') { +# return 'text/x-vcalendar'; +# } + switch($type) { + case 'contacts': + return 'text/x-vcard'; + break; + + case 'sifcalendar': + case './sifcalendar': + return 'text/x-s4j-sife'; + break; + + case 'sifcontacts': + case './sifcontacts': + return 'text/x-s4j-sifc'; + break; + + case 'siftasks': + case './siftasks': + return 'text/x-s4j-sift'; + break; + + case 'notes': + return 'text/x-vnote'; + break; + + case 'tasks': + return 'text/x-vcalendar'; + break; + + case 'calendar': + return 'text/x-vcalendar'; + break; + } + } + + /** + * Returns the preferred contenttype of the client for the given + * sync data type (database). + * + * This is passed as an option to the Horde API export functions. + */ + function getPreferedContentTypeClient($_sourceLocURI) + { + $deviceInfo = $this->getClientDeviceInfo(); + + if(isset($deviceInfo['dataStore'][$_sourceLocURI]['rxPreference']['contentType'])) + { + return array('ContentType' => $deviceInfo['dataStore'][$_sourceLocURI]['rxPreference']['contentType']); + } + + Horde::logMessage('SyncML: sourceLocURI ' . $_sourceLocURI .' not found', __FILE__, __LINE__, PEAR_LOG_DEBUG); + return PEAR::raiseError(_('sourceLocURI not found')); +# elseif ($type == 'contacts') { +# return 'text/x-vcard'; +# } elseif ($type == 'notes') { +# return array('ContentType' => 'text/x-vnote', +# 'ENCODING' => 'QUOTED-PRINTABLE', +# 'CHARSET' => 'UTF-8'); +# } elseif ($type == 'tasks') { +# return 'text/x-vcalendar'; +# } elseif ($type == 'calendar') { +# return array('ContentType' => 'text/x-vcalendar', +# 'ENCODING' => 'QUOTED-PRINTABLE', +# 'CHARSET' => 'UTF-8'); +# } + } + + function setClientAnchorNext($type, $a) + { + $this->_clientAnchorNext[$type] = $a; + } + + function setServerAnchorLast($type, $a) + { + $this->_serverAnchorLast[$type] = $a; + } + + function setServerAnchorNext($type, $a) + { + $this->_serverAnchorNext[$type] = $a; + } + + function getClientAnchorNext($type) + { + return $this->_clientAnchorNext[$type]; + } + + function getServerAnchorNext($type) + { + return $this->_serverAnchorNext[$type]; + } + + function getServerAnchorLast($type) + { + return $this->_serverAnchorLast[$type]; + } + + /** + * Retrieves information about the previous sync if any. Returns + * false if no info found or a DateTreeObject with at least the + * following attributes: + * + * ClientAnchor: the clients Next Anchor of the previous sync. + * ServerAnchor: the Server Next Anchor of the previous sync. + */ + function &getSyncSummary($type) + { + $dt = &$this->getDataTree(); + + $id = $dt->getId($this->_locName . $this->_sourceURI . $type . 'syncSummary'); + if (is_a($id, 'PEAR_Error')) { + return false; + } + + return $dt->getObjectById($id); + } + + /** + * Retrieves information about the clients device info if any. Returns + * false if no info found or a DateTreeObject with at least the + * following attributes: + * + * a array containing all available infos about the device + */ + function getClientDeviceInfo() + { + $dt = &$this->getDataTree(); + + $id = $dt->getId($this->_locName . $this->_sourceURI . 'deviceInfo'); + if (is_a($id, 'PEAR_Error')) { + return false; + } + + $info = $dt->getObjectById($id); + + return $info->get('ClientDeviceInfo'); + } + + /** + * write clients device info to database + */ + function writeClientDeviceInfo() + { + if (!isset($this->_clientDeviceInfo) || !is_array($this->_clientDeviceInfo)) { + return; + } + + $dt = &$this->getDataTree(); + + $s = $this->_locName . $this->_sourceURI . 'deviceInfo'; + + // Set $locid. + $info = &new DataTreeObject($s); + $info->set('ClientDeviceInfo', $this->_clientDeviceInfo); + $r = $dt->add($info); + if (is_a($r, 'PEAR_Error')) { + // Object already exists: update instead. + $dt->updateData($info); + } + } + + /** + * After a successful sync, the client and server's Next Anchors + * are written to the database so they can be used to negotiate + * upcoming syncs. + */ + function writeSyncSummary() + { + if (!isset($this->_serverAnchorNext) || !is_array($this->_serverAnchorNext)) { + return; + } + + $dt = &$this->getDataTree(); + + foreach (array_keys($this->_serverAnchorNext) as $type) { + $s = $this->_locName . $this->_sourceURI . $type . 'syncSummary'; + + // Set $locid. + $info = &new DataTreeObject($s); + $info->set('ClientAnchor', $this->_clientAnchorNext); + $info->set('ServerAnchor', $this->_serverAnchorNext); + $r = $dt->add($info); + if (is_a($r, 'PEAR_Error')) { + // Object already exists: update instead. + $dt->updateData($info); + } + } + } + + /** + * The log simply counts the entries for each topic. + */ + function log($topic) + { + if (isset($this->_log[$topic])) { + $this->_log[$topic] += 1; + } else { + $this->_log[$topic] = 1; + } + } + + /** + * The Log is an array where the key is the event name and the + * value says how often this event occured. + */ + function getLog() + { + return $this->_log; + } + + /** + * Convert the content. + * + * Currently strips uid (primary key) information as client and + * server might use different ones. + * + * Charset conversions might be added here too. + */ + function convertClient2Server($content, $contentType) + { + switch ($contentType) { + case 'text/calendar': + case 'text/x-icalendar': + case 'text/x-vcalendar': + case 'text/x-vevent': + case 'text/x-vtodo': + $content = preg_replace('/^UID:.*\n/m', '', $content, 1); + break; + } + + return $content; + } + + /** + * Convert the content. + * + * Currently strips uid (primary key) information as client and + * server might use different ones. + * + * Charset conversions might be added here too. + */ + function convertServer2Client($content, $contentType) + { + switch ($contentType) { + case 'text/calendar': + case 'text/x-icalendar': + case 'text/x-vcalendar': + case 'text/x-vevent': + case 'text/x-vtodo': + $content = preg_replace('/^UID:.*\n/m', '', $content, 1); + break; + } + + // FIXME: utf8 really should be fine. But the P900 seems to + // expect ISO 8559 even when <?xml version="1.0" + // encoding="utf-8"> is specified. + // + // So at least make this dependant on the device information. + return utf8_decode($content); + } + + /** + * When True, Task Item changes (NAG) are sent to the server + * during "calendar" Syncs. That's the way the P800/900 handles + * things. Should be retrieved from devinf? + */ + function handleTasksInCalendar() + { + return false; + } + + /** + * This is a small helper function that can be included to check + * whether a given $obj is a PEAR_Error or not. If so, it logs + * to debug, var_dumps the $obj and exits. + */ + function dieOnError($obj, $file = __FILE__, $line = __LINE__) + { + if (!is_a($obj, 'PEAR_Error')) { + return; + } + + Horde::logMessage('SyncML: PEAR Error: ' . $obj->getMessage(), $file, $line, PEAR_LOG_ERR); + print "PEAR ERROR\n\n"; + var_dump($obj); + exit; + } + + function getAlert222Received() { + return $this->_receivedAlert222; + } + + function setAlert222Received($_status) { + $this->_receivedAlert222 = (bool)$_status; + } + +} diff --git a/phpgwapi/inc/horde/XML/WBXML/Decoder.php b/phpgwapi/inc/horde/XML/WBXML/Decoder.php new file mode 100644 index 0000000000..dfbe9f1c22 --- /dev/null +++ b/phpgwapi/inc/horde/XML/WBXML/Decoder.php @@ -0,0 +1,668 @@ + + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * From Binary XML Content Format Specification Version 1.3, 25 July + * 2001 found at http://www.wapforum.org + * + * @package XML_WBXML + */ +class XML_WBXML_Decoder extends XML_WBXML_ContentHandler { + + /** + * Document Public Identifier type + * 1 mb_u_int32 well known type + * 2 string table + * from spec but converted into a string. + * + * Document Public Identifier + * Used with dpiType. + */ + var $_dpi; + + /** + * String table as defined in 5.7 + */ + var $_stringTable = array(); + + /** + * Content handler. + * Currently just outputs raw XML. + */ + var $_ch; + + var $_tagDTD; + + var $_prevAttributeDTD; + + var $_attributeDTD; + + /** + * State variables. + */ + var $_tagStack = array(); + var $_isAttribute; + var $_isData = false; + + var $_error = false; + + /** + * The DTD Manager. + * + * @var XML_WBXML_DTDManager + */ + var $_dtdManager; + + /** + * The string position. + * + * @var integer + */ + var $_strpos; + + /** + * Constructor. + */ + function XML_WBXML_Decoder() + { + $this->_dtdManager = &new XML_WBXML_DTDManager(); + } + + /** + * Sets the contentHandler that will receive the output of the + * decoding. + * + * @param XML_WBXML_ContentHandler $ch The contentHandler + */ + function setContentHandler(&$ch) { + $this->_ch = &$ch; + } + /** + * Return one byte from the input stream. + * + * @param string $input The WBXML input string. + */ + function getByte($input) + { + $value = $input{$this->_strpos++}; + $value = ord($value); + + return $value; + } + + /** + * Takes a WBXML input document and returns decoded XML. + * However the preferred and more effecient method is to + * use decode() rather than decodeToString() and have an + * appropriate contentHandler deal with the decoded data. + * + * @param string $wbxml The WBXML document to decode. + * + * @return string The decoded XML document. + */ + function decodeToString($wbxml) + { + $this->_ch = &new XML_WBXML_ContentHandler(); + + $r = $this->decode($wbxml); + if (is_a($r, 'PEAR_Error')) { + return $r; + } + return $this->_ch->getOutput(); + } + + /** + * Takes a WBXML input document and decodes it. + * Decoding result is directly passed to the contentHandler. + * A contenthandler must be set using setContentHandler + * prior to invocation of this method + * + * @param string $wbxml The WBXML document to decode. + * + * @return mixed True on success or PEAR_Error. + */ + function decode($wbxml) + { + // fix for Nokia Series 60 which seem to send empty data block sometimes + if(strlen($wbxml) == 0) { + return true; + } + + $this->_error = false; // reset state + $this->_strpos = 0; + + if (empty($this->_ch)) { + return $this->raiseError('No Contenthandler defined.'); + } + + // Get Version Number from Section 5.4 + // version = u_int8 + // currently 1, 2 or 3 + $this->_wbxmlVersion = $this->getVersionNumber($wbxml); + + // Get Document Public Idetifier from Section 5.5 + // publicid = mb_u_int32 | (zero index) + // zero = u_int8 + // Containing the value zero (0) + // The actual DPI is determined after the String Table is read. + $dpiStruct = $this->getDocumentPublicIdentifier($wbxml); + // Get Charset from 5.6 + // charset = mb_u_int32 + $this->_charset = $this->getCharset($wbxml); + + // Get String Table from 5.7 + // strb1 = length *byte + $this->retrieveStringTable($wbxml); + + // Get Document Public Idetifier from Section 5.5. + $this->_dpi = $this->getDocumentPublicIdentifierImpl($dpiStruct['dpiType'], + $dpiStruct['dpiNumber'], + $this->_stringTable); + + // Now the real fun begins. + // From Sections 5.2 and 5.8 + + + // Default content handler. + $this->_dtdManager = &new XML_WBXML_DTDManager(); + + // Get the starting DTD. + $this->_tagDTD = $this->_dtdManager->getInstance($this->_dpi); + + if (!$this->_tagDTD) { + return $this->raiseError('No DTD found for ' + . $this->_dpi . '/' + . $dpiStruct['dpiNumber']); + } + + $this->_attributeDTD = $this->_tagDTD; + + while (empty($this->_error) && $this->_strpos < strlen($wbxml)) { + $this->_decode($wbxml); + } + if (!empty($this->_error)) { + return $this->_error; + } + return true; + } + + function getVersionNumber($input) + { + return $this->getByte($input); + } + + function getDocumentPublicIdentifier($input) + { + $i = XML_WBXML::MBUInt32ToInt($input, $this->_strpos); + if ($i == 0) { + return array('dpiType' => 2, + 'dpiNumber' => $this->getByte($input)); + } else { + return array('dpiType' => 1, + 'dpiNumber' => $i); + } + } + + function getDocumentPublicIdentifierImpl($dpiType, $dpiNumber) + { + if ($dpiType == 1) { + return XML_WBXML::getDPIString($dpiNumber); + } else { + return $this->getStringTableEntry($dpiNumber); + } + } + + /** + * Returns the character encoding. Only default character + * encodings from J2SE are supported. From + * http://www.iana.org/assignments/character-sets and + * http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html + */ + function getCharset($input) + { + $cs = XML_WBXML::MBUInt32ToInt($input, $this->_strpos); + return XML_WBXML::getCharsetString($cs); + } + + /** + * Retrieves the string table. + * The string table consists of an mb_u_int32 length + * and then length bytes forming the table. + * References to the string table refer to the + * starting position of the (null terminated) + * string in this table. + */ + function retrieveStringTable($input) + { + $size = XML_WBXML::MBUInt32ToInt($input, $this->_strpos); + $this->_stringTable = substr($input, $this->_strpos, $size); + $this->_strpos += $size; + // print "stringtable($size):" . $this->_stringTable ."\n"; + } + + function getStringTableEntry($index) + { + if ($index >= strlen($this->_stringTable)) { + $this->_error = + $this->_ch->raiseError('Invalid offset ' . $index + . ' value encountered around position ' + . $this->_strpos + . '. Broken wbxml?'); + return ''; + } + + // copy of method termstr but without modification of this->_strpos + + $str = '#'; // must start with nonempty string to allow array access + + $i = 0; + $ch = $this->_stringTable[$index++]; + if (ord($ch) == 0) { + return ''; // don't return '#' + } + + while (ord($ch) != 0) { + $str[$i++] = $ch; + if ($index >= strlen($this->_stringTable)) { + break; + } + $ch = $this->_stringTable[$index++]; + } + // print "string table entry: $str\n"; + return $str; + + } + + function _decode($input) + { + $token = $this->getByte($input); + $str = ''; + + #print "position: " . $this->_strpos . " token: " . $token . " str10: " . substr($input, $this->_strpos, 10) . "\n"; // @todo: remove debug output + + switch ($token) { + case XML_WBXML_GLOBAL_TOKEN_STR_I: + // Section 5.8.4.1 + $str = $this->termstr($input); + $this->_ch->characters($str); + // print "str:$str\n"; // @TODO Remove debug code + break; + + case XML_WBXML_GLOBAL_TOKEN_STR_T: + // Section 5.8.4.1 + $x = XML_WBXML::MBUInt32ToInt($input, $this->_strpos); + $str = $this->getStringTableEntry($x); + $this->_ch->characters($str); + break; + + case XML_WBXML_GLOBAL_TOKEN_EXT_I_0: + case XML_WBXML_GLOBAL_TOKEN_EXT_I_1: + case XML_WBXML_GLOBAL_TOKEN_EXT_I_2: + // Section 5.8.4.2 + $str = $this->termstr($input); + $this->_ch->characters($str); + break; + + case XML_WBXML_GLOBAL_TOKEN_EXT_T_0: + case XML_WBXML_GLOBAL_TOKEN_EXT_T_1: + case XML_WBXML_GLOBAL_TOKEN_EXT_T_2: + // Section 5.8.4.2 + $str = $this->getStringTableEnty(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + $this->_ch->characters($str); + break; + + case XML_WBXML_GLOBAL_TOKEN_EXT_0: + case XML_WBXML_GLOBAL_TOKEN_EXT_1: + case XML_WBXML_GLOBAL_TOKEN_EXT_2: + // Section 5.8.4.2 + $extension = $this->getByte($input); + $this->_ch->characters($extension); + break; + + case XML_WBXML_GLOBAL_TOKEN_ENTITY: + // Section 5.8.4.3 + // UCS-4 chracter encoding? + $entity = $this->entity(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + + $this->_ch->characters('&#' . $entity . ';'); + break; + + case XML_WBXML_GLOBAL_TOKEN_PI: + // Section 5.8.4.4 + // throw new IOException + // die("WBXML global token processing instruction(PI, " + token + ") is unsupported!\n"); + break; + + case XML_WBXML_GLOBAL_TOKEN_LITERAL: + // Section 5.8.4.5 + $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + $this->parseTag($input, $str, false, false); + break; + + case XML_WBXML_GLOBAL_TOKEN_LITERAL_A: + // Section 5.8.4.5 + $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + $this->parseTag($input, $str, true, false); + break; + + case XML_WBXML_GLOBAL_TOKEN_LITERAL_AC: + // Section 5.8.4.5 + $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + $this->parseTag($input, $string, true, true); + break; + + case XML_WBXML_GLOBAL_TOKEN_LITERAL_C: + // Section 5.8.4.5 + $str = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + $this->parseTag($input, $str, false, true); + break; + + case XML_WBXML_GLOBAL_TOKEN_OPAQUE: + // Section 5.8.4.6 + $size = XML_WBXML::MBUInt32ToInt($input, $this->_strpos); + // print "opaque of size $size\n"; // @todo remove debug + $b = substr($input, $this->_strpos, $size); + #$b = mb_substr($input, $this->_strpos, $size, 'ISO-8859-1'); + $this->_strpos += $size; + + // opaque data inside a element may or may not be + // a nested wbxml document (for example devinf data). + // We find out by checking the first byte of the data: if it's + // 1, 2 or 3 we expect it to be the version number of a wbxml + // document and thus start a new wbxml decoder instance on it. + + if ($this->_isData && ord($b) <= 10) { + $decoder = &new XML_WBXML_Decoder(true); + $decoder->setContentHandler($this->_ch); + $s = $decoder->decode($b); + // /* // @todo: FIXME currently we can't decode Nokia + // DevInf data. So ignore error for the time beeing. + if (is_a($s, 'PEAR_Error')) { + $this->_error = $s; + return; + } + // */ + // $this->_ch->characters($s); + } else { + /* normal opaque behaviour: just copy the raw data: */ + $this->_ch->characters( $b); + } + + // old approach to deal with opaque data inside ContentHandler: + // FIXME Opaque is used by SYNCML. Opaque data that depends on the context + // if (contentHandler instanceof OpaqueContentHandler) { + // ((OpaqueContentHandler)contentHandler).opaque(b); + // } else { + // String str = new String(b, 0, size, charset); + // char[] chars = str.toCharArray(); + + // contentHandler.characters(chars, 0, chars.length); + // } + + break; + + case XML_WBXML_GLOBAL_TOKEN_END: + // Section 5.8.4.7.1 + $str = $this->endTag(); + break; + + case XML_WBXML_GLOBAL_TOKEN_SWITCH_PAGE: + // Section 5.8.4.7.2 + $codePage = $this->getByte($input); + // print "switch to codepage $codePage\n"; // @todo: remove debug code + $this->switchElementCodePage($codePage); + break; + + default: + // Section 5.8.2 + // Section 5.8.3 + $hasAttributes = (($token & 0x80) != 0); + $hasContent = (($token & 0x40) != 0); + $realToken = $token & 0x3F; + $str = $this->getTag($realToken); + + // print "element:$str\n"; // @TODO Remove debug code + $this->parseTag($input, $str, $hasAttributes, $hasContent); + + if ($realToken == 0x0f) { + // store if we're inside a Data tag. This may contain + // an additional enclosed wbxml document on which we have + // to run a seperate encoder + $this->_isData = true; + } else { + $this->_isData = false; + } + break; + } + } + + function parseTag($input, $tag, $hasAttributes, $hasContent) + { + $attrs = array(); + if ($hasAttributes) { + $attrs = $this->getAttributes($input); + } + + $this->_ch->startElement($this->getCurrentURI(), $tag, $attrs); + + if ($hasContent) { + // FIXME I forgot what does this does. Not sure if this is + // right? + $this->_tagStack[] = $tag; + } else { + $this->_ch->endElement($this->getCurrentURI(), $tag); + } + } + + function endTag() + { + if (count($this->_tagStack)) { + $tag = array_pop($this->_tagStack); + } else { + $tag = 'Unknown'; + } + + $this->_ch->endElement($this->getCurrentURI(), $tag); + + return $tag; + } + + function getAttributes($input) + { + $this->startGetAttributes(); + $hasMoreAttributes = true; + + $attrs = array(); + $attr = null; + $value = null; + $token = null; + + while ($hasMoreAttributes) { + $token = $this->getByte($input); + + switch ($token) { + // Attribute specified. + case XML_WBXML_GLOBAL_TOKEN_LITERAL: + // Section 5.8.4.5 + if (isset($attr)) { + $attrs[] = array('attribute' => $attr, + 'value' => $value); + } + + $attr = $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + break; + + // Value specified. + case XML_WBXML_GLOBAL_TOKEN_EXT_I_0: + case XML_WBXML_GLOBAL_TOKEN_EXT_I_1: + case XML_WBXML_GLOBAL_TOKEN_EXT_I_2: + // Section 5.8.4.2 + $value .= $this->termstr($input); + break; + + case XML_WBXML_GLOBAL_TOKEN_EXT_T_0: + case XML_WBXML_GLOBAL_TOKEN_EXT_T_1: + case XML_WBXML_GLOBAL_TOKEN_EXT_T_2: + // Section 5.8.4.2 + $value .= $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + break; + + case XML_WBXML_GLOBAL_TOKEN_EXT_0: + case XML_WBXML_GLOBAL_TOKEN_EXT_1: + case XML_WBXML_GLOBAL_TOKEN_EXT_2: + // Section 5.8.4.2 + $value .= $input[$this->_strpos++]; + break; + + case XML_WBXML_GLOBAL_TOKEN_ENTITY: + // Section 5.8.4.3 + $value .= $this->entity(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + break; + + case XML_WBXML_GLOBAL_TOKEN_STR_I: + // Section 5.8.4.1 + $value .= $this->termstr($input); + break; + + case XML_WBXML_GLOBAL_TOKEN_STR_T: + // Section 5.8.4.1 + $value .= $this->getStringTableEntry(XML_WBXML::MBUInt32ToInt($input, $this->_strpos)); + break; + + case XML_WBXML_GLOBAL_TOKEN_OPAQUE: + // Section 5.8.4.6 + $size = XML_WBXML::MBUInt32ToInt($input, $this->_strpos); + $b = substr($input, $this->_strpos, $this->_strpos + $size); + $this->_strpos += $size; + + $value .= $b; + break; + + case XML_WBXML_GLOBAL_TOKEN_END: + // Section 5.8.4.7.1 + $hasMoreAttributes = false; + if (isset($attr)) { + $attrs[] = array('attribute' => $attr, + 'value' => $value); + } + break; + + case XML_WBXML_GLOBAL_TOKEN_SWITCH_PAGE: + // Section 5.8.4.7.2 + $codePage = $this->getByte($input); + if (!$this->_prevAttributeDTD) { + $this->_prevAttributeDTD = $this->_attributeDTD; + } + + $this->switchAttributeCodePage($codePage); + break; + + default: + if ($token > 128) { + if (isset($attr)) { + $attrs[] = array('attribute' => $attr, + 'value' => $value); + } + $attr = $this->_attributeDTD->toAttribute($token); + } else { + // Value. + $value .= $this->_attributeDTD->toAttribute($token); + } + break; + } + } + + if (!$this->_prevAttributeDTD) { + $this->_attributeDTD = $this->_prevAttributeDTD; + $this->_prevAttributeDTD = false; + } + + $this->stopGetAttributes(); + } + + function startGetAttributes() + { + $this->_isAttribute = true; + } + + function stopGetAttributes() + { + $this->_isAttribute = false; + } + + function getCurrentURI() + { + if ($this->_isAttribute) { + return $this->_tagDTD->getURI(); + } else { + return $this->_attributeDTD->getURI(); + } + } + + function writeString($str) + { + $this->_ch->characters($str); + } + + function getTag($tag) + { + // Should know which state it is in. + return $this->_tagDTD->toTagStr($tag); + } + + function getAttribute($attribute) + { + // Should know which state it is in. + $this->_attributeDTD->toAttributeInt($attribute); + } + + function switchElementCodePage($codePage) + { + $this->_tagDTD = &$this->_dtdManager->getInstance($this->_tagDTD->toCodePageStr($codePage)); + $this->switchAttributeCodePage($codePage); + } + + function switchAttributeCodePage($codePage) + { + $this->_attributeDTD = &$this->_dtdManager->getInstance($this->_attributeDTD->toCodePageStr($codePage)); + } + + /** + * Return the hex version of the base 10 $entity. + */ + function entity($entity) + { + return dechex($entity); + } + + /** + * Reads a null terminated string. + */ + function termstr($input) + { + $str = '#'; // must start with nonempty string to allow array access + $i = 0; + $ch = $input[$this->_strpos++]; + if (ord($ch) == 0) { + return ''; // don't return '#' + } + while (ord($ch) != 0) { + $str[$i++] = $ch; + $ch = $input[$this->_strpos++]; + } + + return $str; + } + +} +