* @author Anthony Mills * @package SyncMl */ class Horde_SyncMl_ContentHandler { /** * Stack for holding the xml elements during creation of the object from * the xml event flow. * * @var array */ protected $_Stack = array(); /** * @var string */ protected $_chars; /** * Instance of Horde_SyncMl_Command. Events are passed through to this * ContentHandler. * * @var Horde_SyncMl_Command */ protected $_currentCommand; /** * Whether we received a final element in this message. */ protected $_gotFinal = false; protected $_xmlWriter; protected $_wbxmlparser = null; /** * The response URI as sent by the server. * * This is the endpoint URL of the RPC server. * * @var string */ protected $_respURI; public $debug = false; public function __construct() { /* Set to true to indicate that we expect another message from the * client. If this is still false at the end of processing, the sync * session is finished and we can close the session. */ $GLOBALS['message_expectresponse'] = false; } /** * Here's were all the processing takes place: gets the SyncML request * data and returns a SyncML response. The only thing that needs to be in * place before invoking this public function is a working backend. * * @param string $request The raw request string. * @param string $contentType The MIME content type of the request. Should * be either application/vnd.syncml or * application/vnd.syncml+wbxml. * @param string $respURI The url of the server endpoint. Will be * returned in the RespURI element. */ public function process($request, $contentType, $respURI = null) { $isWBXML = $contentType =='application/vnd.syncml+wbxml'; $this->_respURI = $respURI; /* Catch any errors/warnings/notices that may get thrown while * processing. Don't want to let anything go to the client that's not * part of the valid response. */ ob_start(); $GLOBALS['backend']->logFile(Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE, $request, $isWBXML); if (!$isWBXML) { /* XML code. */ /* try to extract charset from XML text */ if (preg_match('/^\s*<\?xml[^>]*encoding\s*=\s*"([^"]*)"/i', $request, $m)) { $charset = $m[1]; } else { $charset = 'UTF-8'; } $GLOBALS['backend']->setCharset($charset); /* Init output handler. */ $this->_xmlWriter = &Horde_SyncMl_XmlOutput::singleton(); /* Horde_Xml_Wbxml_ContentHandler Is a class that produces plain XML * output. */ $this->_xmlWriter->init(new Horde_Xml_Wbxml_ContentHandler()); /* Create the XML parser and set method references. */ $parser = xml_parser_create_ns($charset); xml_set_object($parser, $this); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); xml_set_element_handler($parser, '_startElement', '_endElement'); xml_set_character_data_handler($parser, '_characters'); xml_set_processing_instruction_handler($parser, ''); xml_set_external_entity_ref_handler($parser, ''); /* Here we go: fire off events: */ if (!xml_parse($parser, $request)) { $s = sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser)); $GLOBALS['backend']->logMessage($s, 'ERR'); xml_parser_free($parser); return new PEAR_Error($s); } xml_parser_free($parser); } else { /* The decoder works like the parser in the XML code above: It * parses the input and calls the callback functions of $this. */ $this->_wbxmlparser = new Horde_Xml_Wbxml_Decoder(); $this->_wbxmlparser->setContentHandler($this); /* Init output handler. */ $this->_xmlWriter = &Horde_SyncMl_XmlOutput::singleton(); $this->_xmlWriter->init(new Horde_Xml_Wbxml_Encoder()); /* Here we go: fire off events: */ /* @todo catch exceptions */ $this->_wbxmlparser->decode($request); } $id = @session_id(); $sessionclose = empty($id); $output = $this->getOutput(); if (!$isWBXML) { $output = '' . $output; } $GLOBALS['backend']->logFile(Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE, $output, $isWBXML, $sessionclose); /* Clear the output buffer that we started above, and log anything * that came up for later debugging. */ $errorLogging = ob_get_clean(); if (!empty($errorLogging)) { $GLOBALS['backend']->logMessage('Caught output: ' . $errorLogging, 'WARN'); } return $output; } /* * CONTENTHANDLER CALLBACK FUNCTIONS * The following functions are callback functions that are called by the * XML parser. The XML and WBXML parsers use slightly different functions, * so the methods are duplicated. */ /** * Returns the XML|WBXML output once processing is finished. * * @return string The XML or WBXML output data. */ public function getOutput() { return $this->_xmlWriter->getOutput(); } /** * Callback public function called by XML parser. */ protected function _startElement($parser, $tag, $attributes) { list($uri, $name) = $this->_splitURI($tag); $this->startElement($uri, $name, $attributes); } /** * Callback public function called by XML parser. */ protected function _characters($parser, $chars) { $this->characters($chars); } /** * Callback public function called by XML parser. */ protected function _endElement($parser, $tag) { list($uri, $name) = $this->_splitURI($tag); $this->endElement($uri, $name); } /** * Splits an URI as provided by the XML parser. */ protected function _splitURI($tag) { $parts = explode(':', $tag); $name = array_pop($parts); $uri = implode(':', $parts); return array($uri, $name); } /** * Callback public function called by WBXML parser. */ public function startElement($uri, $element, $attrs) { $this->_Stack[] = $element; // : don't do anyhting yet if (count($this->_Stack) == 1) { return; } // header or body? if ($this->_Stack[1] == 'SyncHdr') { if (count($this->_Stack) == 2) { $this->_currentCommand = new Horde_SyncMl_Command_SyncHdr($this->_xmlWriter); } $this->_currentCommand->startElement($uri, $element, $attrs); } else { switch (count($this->_Stack)) { case 2: // : do nothing yet break; case 3: // new Command: // <[Command]> $this->_currentCommand = &Horde_SyncMl_Command::factory($element,$this->_xmlWriter); $this->_currentCommand->startElement($uri, $element, $attrs); break; default: // pass on to current command handler: // <...> $this->_currentCommand->startElement($uri, $element, $attrs); break; } } } /** * Callback public function called by WBXML parser. */ public function endElement($uri, $element) { // : everything done already by end of SyncBody if (count($this->_Stack) == 1) { return; } // header or body? if ($this->_Stack[1] == 'SyncHdr') { switch (count($this->_Stack)) { case 2: // end of header $this->handleHeader($this->_currentCommand); if ($this->debug) { var_dump($this->_currentCommand); } unset($this->_currentCommand); break; default: // pass on to command handler: $this->_currentCommand->endElement($uri, $element); break; } } else { switch (count($this->_Stack)) { case 2: // end of SyncBody. Finish everything: $this->handleEnd(); break; case 3: // // Command finished. Complete parsing and pass on to Handler $this->_currentCommand->endElement($uri, $element); $this->handleCommand($this->_currentCommand); if ($this->debug) { var_dump($this->_currentCommand); } unset($this->_currentCommand); break; default: // // pass on to command handler: $this->_currentCommand->endElement($uri, $element); break; } } if (isset($this->_chars)) { unset($this->_chars); } array_pop($this->_Stack); } /** * Callback public function called by WBXML parser. */ public function characters($str) { if (isset($this->_currentCommand)) { $this->_currentCommand->characters($str); } else { if (isset($this->_chars)) { $this->_chars = $this->_chars . $str; } else { $this->_chars = $str; } } } /* * PROCESSING FUNCTIONS * * The following functions are called by the callback functions * and do the actual processing. */ /** * Handles the header logic. * * Invoked after header is parsed. */ public function handleHeader(&$hdr) { if (is_object($this->_wbxmlparser)) { /* The WBXML parser only knows about the charset once parsing is * started. So setup charset now. */ $this->_xmlWriter->output->setVersion($this->_wbxmlparser->getVersion()); $this->_xmlWriter->output->setCharset($this->_wbxmlparser->getCharsetStr()); $GLOBALS['backend']->setCharset($this->_wbxmlparser->getCharsetStr()); } /* Start the session. */ $hdr->setupState(); $state = $GLOBALS['backend']->state; $state->wbxml = $this->_xmlWriter->isWBXML(); /* Check auth. */ if (!$state->authenticated) { $auth = $GLOBALS['backend']->checkAuthentication( $hdr->user, $hdr->credData, $hdr->credFormat, $hdr->credType); if ($auth !== false) { $state->authenticated = true; $statuscode = Horde_SyncMl::RESPONSE_AUTHENTICATION_ACCEPTED; $state->user = $auth; $GLOBALS['backend']->setUser($auth); } else { if (!$hdr->credData) { $statuscode = Horde_SyncMl::RESPONSE_CREDENTIALS_MISSING; } else { $statuscode = Horde_SyncMl::RESPONSE_INVALID_CREDENTIALS; } $GLOBALS['backend']->logMessage('Invalid authentication', 'DEBUG'); } } else { $statuscode = Horde_SyncMl::RESPONSE_OK; $GLOBALS['backend']->setUser($state->user); } /* Create . */ $this->_xmlWriter->outputInit(); /* Got the state; now write our SyncHdr header. */ $this->_xmlWriter->outputHeader($this->_respURI); /* Creates . */ $this->_xmlWriter->outputBodyStart(); /* Output status for SyncHdr. */ $this->_xmlWriter->outputStatus('0', 'SyncHdr', $statuscode, $state->targetURI, $state->sourceURI); /* Debug logging string. */ $str = 'Authenticated: ' . ($state->authenticated ? 'yes' : 'no') . '; version: ' . $state->getVerDTD() . '; message ID: ' . $state->messageID . '; source URI: ' . $state->sourceURI . '; target URI: ' . $state->targetURI . '; user: ' . $state->user . '; charset: ' . $GLOBALS['backend']->getCharset() . '; wbxml: ' . ($state->wbxml ? 'yes' : 'no'); $GLOBALS['backend']->logMessage($str, 'DEBUG'); } /** * Processes one command after it has been completely parsed. * * Invoked after a command is parsed. */ public function handleCommand(&$cmd) { $name = $cmd->getCommandName(); if ($name != 'Status' && $name != 'Map' && $name != 'Final' && $name != 'Sync' && $name != 'Results') { /* We've got to do something! This can't be the last packet. */ $GLOBALS['message_expectresponse'] = true; } if ($name == 'Final') { $this->_gotFinal = true; } /* Actual processing takes place here. */ $cmd->handleCommand($this->debug); } /** * Finishes the response. * * Invoked after complete message is parsed. */ public function handleEnd() { global $messageFull; $state = $GLOBALS['backend']->state; /* If there's pending sync data and space left in the message, send * data now. */ if ($messageFull || $state->hasPendingSyncs()) { /* still something to do: don't close session. */ $GLOBALS['message_expectresponse'] = true; } if (!$messageFull && count($p = $state->getPendingSyncs()) > 0) { foreach ($p as $pendingSync) { if (!$messageFull) { $GLOBALS['backend']->logMessage( 'Continuing sync for syncType ' . $pendingSync, 'DEBUG'); $sync = &$state->getSync($pendingSync); $sync->createSyncOutput($this->_xmlWriter); } } } if (isset($state->curSyncItem)) { $this->_xmlWriter->outputAlert( Horde_SyncMl::ALERT_NO_END_OF_DATA, $state->curSyncItem->sync->getClientLocURI(), $state->curSyncItem->sync->getServerLocURI(), $state->curSyncItem->sync->getServerAnchorLast(), $state->curSyncItem->sync->getServerAnchorNext()); } /* Don't send the final tag if we haven't sent all sync data yet. */ if ($this->_gotFinal) { if (!$messageFull && !$state->hasPendingSyncs()) { /* Create . */ $this->_xmlWriter->outputFinal(); $GLOBALS['backend']->logMessage('Sending to client', 'DEBUG'); $state->delayedFinal = false; } else { $GLOBALS['message_expectresponse'] = true; /* Remember to send a Final. */ $state->delayedFinal = true; } } elseif ($state->delayedFinal) { if (!$messageFull && !$state->hasPendingSyncs()) { /* Create . */ $this->_xmlWriter->outputFinal(); $GLOBALS['backend']->logMessage( 'Sending delayed to client', 'DEBUG'); $state->delayedFinal = false; } else { $GLOBALS['message_expectresponse'] = true; } } /* Create . Message is finished now! */ $this->_xmlWriter->outputEnd(); if ($this->_gotFinal && !$GLOBALS['message_expectresponse'] && $state->isAllSyncsComplete()) { /* This packet did not contain any real actions, just status and * map. This means we're done. The session can be closed and the * anchors saved for the next sync. */ foreach ($state->getSyncs() as $sync) { $sync->closeSync(); } $GLOBALS['backend']->logMessage('Session completed and closed', 'DEBUG'); /* Session can be closed here. */ $GLOBALS['backend']->sessionClose(); } else { $GLOBALS['backend']->logMessage('Return message completed', 'DEBUG'); } } }