diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php index f65abba..d7654e0 100644 --- a/lib/ext/Syncroton/Server.php +++ b/lib/ext/Syncroton/Server.php @@ -1,453 +1,456 @@ */ /** * class to handle incoming http ActiveSync requests * * @package Syncroton */ class Syncroton_Server { const PARAMETER_ATTACHMENTNAME = 0; const PARAMETER_COLLECTIONID = 1; const PARAMETER_ITEMID = 3; const PARAMETER_OPTIONS = 7; const MAX_HEARTBEAT_INTERVAL = 3540; // 59 minutes protected $_body; /** * informations about the currently device * * @var Syncroton_Backend_IDevice */ protected $_deviceBackend; /** * @var Zend_Log */ protected $_logger; /** * @var Zend_Controller_Request_Http */ protected $_request; protected $_userId; public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null) { if (Syncroton_Registry::isRegistered('loggerBackend')) { $this->_logger = Syncroton_Registry::get('loggerBackend'); } $this->_userId = $userId; $this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http(); $this->_body = $body !== null ? $body : fopen('php://input', 'r'); $this->_deviceBackend = Syncroton_Registry::getDeviceBackend(); } public function handle() { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod()); switch($this->_request->getMethod()) { case 'OPTIONS': $this->_handleOptions(); break; case 'POST': $this->_handlePost(); break; case 'GET': echo "It works!
Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}."; break; } } /** * handle options request */ protected function _handleOptions() { $command = new Syncroton_Command_Options(); $this->_sendHeaders($command->getHeaders()); } protected function _sendHeaders(array $headers) { foreach ($headers as $name => $value) { header($name . ': ' . $value); } } /** * handle post request */ protected function _handlePost() { $requestParameters = $this->_getRequestParameters($this->_request); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true)); $className = 'Syncroton_Command_' . $requestParameters['command']; if(!class_exists($className)) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']); header("HTTP/1.1 501 not implemented"); return; } // get user device $device = $this->_getUserDevice($this->_userId, $requestParameters); if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') { // decode wbxml request try { $decoder = new Syncroton_Wbxml_Decoder($this->_body); $requestBody = $decoder->decode(); if ($this->_logger instanceof Zend_Log) { $requestBody->formatOutput = true; $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML()); } } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) { $requestBody = NULL; } } else { $requestBody = $this->_body; } header("MS-Server-ActiveSync: 14.00.0536.000"); // avoid sending HTTP header "Content-Type: text/html" for empty sync responses ini_set('default_mimetype', 'application/vnd.ms-sync.wbxml'); try { $command = new $className($requestBody, $device, $requestParameters); $response = $command->handle(); if (!$response) { $response = $command->getResponse(); } } catch (Syncroton_Exception_ProvisioningNeeded $sepn) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed"); header("HTTP/1.1 449 Retry after sending a PROVISION command"); if (version_compare($device->acsversion, '14.0', '>=')) { $response = $sepn->domDocument; } else { // pre 14.0 method return; } } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e)); if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage()); if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString()); header("HTTP/1.1 500 Internal server error"); return; } if ($response instanceof DOMDocument) { if ($this->_logger instanceof Zend_Log) { $this->_logDomDocument(Zend_Log::DEBUG, $response, __METHOD__, __LINE__); } if (isset($command) && $command instanceof Syncroton_Command_ICommand) { $this->_sendHeaders($command->getHeaders()); } $outputStream = fopen("php://temp", 'r+'); $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); try { $encoder->encode($response); } catch (Syncroton_Wbxml_Exception $swe) { if ($this->_logger instanceof Zend_Log) { $this->_logger->err(__METHOD__ . '::' . __LINE__ . " Could not encode output: " . $swe); $this->_logDomDocument(Zend_Log::WARN, $response, __METHOD__, __LINE__); } header("HTTP/1.1 500 Internal server error"); return; } if ($requestParameters['acceptMultipart'] == true) { $parts = $command->getParts(); // output multipartheader $bodyPartCount = 1 + count($parts); // number of parts (4 bytes) $header = pack('i', $bodyPartCount); $partOffset = 4 + (($bodyPartCount * 2) * 4); // wbxml body start and length $streamStat = fstat($outputStream); $header .= pack('ii', $partOffset, $streamStat['size']); $partOffset += $streamStat['size']; // calculate start and length of parts foreach ($parts as $partId => $partStream) { rewind($partStream); $streamStat = fstat($partStream); // part start and length $header .= pack('ii', $partOffset, $streamStat['size']); $partOffset += $streamStat['size']; } echo $header; } // output body rewind($outputStream); fpassthru($outputStream); // output multiparts if (isset($parts)) { foreach ($parts as $partStream) { rewind($partStream); fpassthru($partStream); } } } } /** * write (possible big) DOMDocument in smaller chunks to log file * * @param unknown $priority * @param DOMDocument $dom * @param string $method * @param int $line */ protected function _logDomDocument($priority, DOMDocument $dom, $method, $line) { $loops = 0; - $tempStream = fopen('php://temp/maxmemory:5242880', 'r+'); - + $tempStream = tmpfile(); + + $meta_data = stream_get_meta_data($tempStream); + $filename = $meta_data["uri"]; + $dom->formatOutput = true; - fwrite($tempStream, $dom->saveXML()); + $dom->save($filename); $dom->formatOutput = false; rewind($tempStream); // log data in 1MByte chunks while (!feof($tempStream)) { $this->_logger->log($method . '::' . $line . " xml response($loops):\n" . fread($tempStream, 1048576), $priority); $loops++; } fclose($tempStream); } /** * return request params * * @return array */ protected function _getRequestParameters(Zend_Controller_Request_Http $request) { if (strpos($request->getRequestUri(), '&') === false) { $commands = array( 0 => 'Sync', 1 => 'SendMail', 2 => 'SmartForward', 3 => 'SmartReply', 4 => 'GetAttachment', 9 => 'FolderSync', 10 => 'FolderCreate', 11 => 'FolderDelete', 12 => 'FolderUpdate', 13 => 'MoveItems', 14 => 'GetItemEstimate', 15 => 'MeetingResponse', 16 => 'Search', 17 => 'Settings', 18 => 'Ping', 19 => 'ItemOperations', 20 => 'Provision', 21 => 'ResolveRecipients', 22 => 'ValidateCert' ); $requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?')); $stream = fopen("php://temp", 'r+'); fwrite($stream, base64_decode($requestParameters)); rewind($stream); // unpack the first 4 bytes $unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4)); // 140 => 14.0 $protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1); $command = $commands[$unpacked['command']]; $locale = $unpacked['locale']; // unpack deviceId $length = ord(fread($stream, 1)); if ($length > 0) { $toUnpack = fread($stream, $length); $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack); $deviceId = $unpacked['string']; } // unpack policyKey $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('Vstring', fread($stream, $length)); $policyKey = $unpacked['string']; } // unpack device type $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $deviceType = $unpacked['string']; } while (! feof($stream)) { $tag = ord(fread($stream, 1)); $length = ord(fread($stream, 1)); switch ($tag) { case self::PARAMETER_ATTACHMENTNAME: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $attachmentName = $unpacked['string']; break; case self::PARAMETER_COLLECTIONID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $collectionId = $unpacked['string']; break; case self::PARAMETER_ITEMID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $itemId = $unpacked['string']; break; case self::PARAMETER_OPTIONS: $options = ord(fread($stream, 1)); $saveInSent = !!($options & 0x01); $acceptMultiPart = !!($options & 0x02); break; default: if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters"); } } $result = array( 'protocolVersion' => $protocolVersion, 'command' => $command, 'deviceId' => $deviceId, 'deviceType' => isset($deviceType) ? $deviceType : null, 'policyKey' => isset($policyKey) ? $policyKey : null, 'saveInSent' => isset($saveInSent) ? $saveInSent : false, 'collectionId' => isset($collectionId) ? $collectionId : null, 'itemId' => isset($itemId) ? $itemId : null, 'attachmentName' => isset($attachmentName) ? $attachmentName : null, 'acceptMultipart' => isset($acceptMultiPart) ? $acceptMultiPart : false ); } else { $result = array( 'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'), 'command' => $request->getQuery('Cmd'), 'deviceId' => $request->getQuery('DeviceId'), 'deviceType' => $request->getQuery('DeviceType'), 'policyKey' => $request->getServer('HTTP_X_MS_POLICYKEY'), 'saveInSent' => $request->getQuery('SaveInSent') == 'T', 'collectionId' => $request->getQuery('CollectionId'), 'itemId' => $request->getQuery('ItemId'), 'attachmentName' => $request->getQuery('AttachmentName'), 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T' ); } $result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']); $result['contentType'] = $request->getServer('CONTENT_TYPE'); return $result; } /** * get existing device of owner or create new device for owner * * @param unknown_type $ownerId * @param unknown_type $deviceId * @param unknown_type $deviceType * @param unknown_type $userAgent * @param unknown_type $protocolVersion * @return Syncroton_Model_Device */ protected function _getUserDevice($ownerId, $requestParameters) { try { $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']); $device->useragent = $requestParameters['userAgent']; $device->acsversion = $requestParameters['protocolVersion']; if ($device->isDirty()) { $device = $this->_deviceBackend->update($device); } } catch (Syncroton_Exception_NotFound $senf) { $device = $this->_deviceBackend->create(new Syncroton_Model_Device(array( 'owner_id' => $ownerId, 'deviceid' => $requestParameters['deviceId'], 'devicetype' => $requestParameters['deviceType'], 'useragent' => $requestParameters['userAgent'], 'acsversion' => $requestParameters['protocolVersion'], 'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null ))); } return $device; } public static function validateSession() { $validatorFunction = Syncroton_Registry::getSessionValidator(); return $validatorFunction(); } } diff --git a/lib/ext/Syncroton/Wbxml/Encoder.php b/lib/ext/Syncroton/Wbxml/Encoder.php index f17db83..9e88db2 100644 --- a/lib/ext/Syncroton/Wbxml/Encoder.php +++ b/lib/ext/Syncroton/Wbxml/Encoder.php @@ -1,369 +1,372 @@ * @version $Id:Encoder.php 4968 2008-10-17 09:09:33Z l.kneschke@metaways.de $ */ /** * class to convert XML to WBXML * * @package Wbxml * @subpackage Wbxml */ class Syncroton_Wbxml_Encoder extends Syncroton_Wbxml_Abstract { /** * stack of dtd objects * * @var array */ protected $_dtdStack = array(); /** * stack of stream resources * * @var array */ protected $_streamStack = array(); /** * stack of levels when to pop data from the other stacks * * @var array */ protected $_popStack = array(); /** * count level of tags * * @var string */ protected $_level = 0; /** * when to take data next time from the different stacks * * @var unknown_type */ protected $_nextStackPop = NULL; /** * collect data trough different calls to _handleCharacters * * @var string */ protected $_currentTagData = NULL; /** * the current tag as read by the parser * * @var string */ protected $_currentTag = NULL; /** * the constructor * * @param resource $_stream * @param string $_charSet * @param integer $_version */ public function __construct($_stream, $_charSet = 'UTF-8', $_version = 2) { $this->_stream = $_stream; $this->_charSet = $_charSet; $this->_version = $_version; } /** * initialize internal variables and write wbxml header to stream * * @param string $_urn * @todo check if dpi > 0, instead checking the urn */ protected function _initialize($_dom) { $this->_dtd = Syncroton_Wbxml_Dtd_Factory::factory($_dom->doctype->name); $this->_codePage = $this->_dtd->getCurrentCodePage(); // the WBXML version $this->_writeByte($this->_version); if($this->_codePage->getDPI() === NULL) { // the document public identifier $this->_writeMultibyteUInt(1); } else { // the document public identifier // defined in string table $this->_writeMultibyteUInt(0); // the offset of the DPI in the string table $this->_writeByte(0); } // write the charSet $this->_writeCharSet($this->_charSet); if($this->_codePage->getDPI() === NULL) { // the length of the string table $this->_writeMultibyteUInt(0); } else { // the length of the string table $this->_writeMultibyteUInt(strlen($this->_codePage->getDPI())); // the dpi $this->_writeString($this->_codePage->getDPI()); } } /** * write charset to stream * * @param string $_charSet * @todo add charset lookup table. currently only utf-8 is supported */ protected function _writeCharSet($_charSet) { switch(strtoupper($_charSet)) { case 'UTF-8': $this->_writeMultibyteUInt(106); break; default: throw new Syncroton_Wbxml_Exception('unsuported charSet ' . strtoupper($_charSet)); break; } } /** * start encoding of xml to wbxml * * @param string $_xml the xml string * @return resource stream */ public function encode(DOMDocument $_dom) { $_dom->formatOutput = false; - $tempStream = fopen('php://temp/maxmemory:5242880', 'r+'); - fwrite($tempStream, $_dom->saveXML()); + $tempStream = tmpfile(); + + $meta_data = stream_get_meta_data($tempStream); + $filename = $meta_data["uri"]; + $_dom->save($filename); rewind($tempStream); $this->_initialize($_dom); $parser = xml_parser_create_ns($this->_charSet, ';'); xml_set_object($parser, $this); xml_set_element_handler($parser, '_handleStartTag', '_handleEndTag'); xml_set_character_data_handler($parser, '_handleCharacters'); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); while (!feof($tempStream)) { if (!xml_parse($parser, fread($tempStream, 1048576), feof($tempStream))) { // uncomment to write xml document to file #rewind($tempStream); #$xmlStream = fopen(tempnam(sys_get_temp_dir(), "xmlerrors"), 'r+'); #stream_copy_to_stream($tempStream, $xmlStream); #fclose($xmlStream); throw new Syncroton_Wbxml_Exception(sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser) )); } } fclose($tempStream); xml_parser_free($parser); } /** * get's called by xml parser when tag starts * * @param resource $_parser * @param string $_tag current tag prefixed with namespace * @param array $_attributes list of tag attributes */ protected function _handleStartTag($_parser, $_tag, $_attributes) { $this->_level++; $this->_currentTagData = null; // write data for previous tag happens whith if($this->_currentTag !== NULL) { $this->_writeTag($this->_currentTag, $this->_attributes, true); } list($nameSpace, $this->_currentTag) = explode(';', $_tag); if($this->_codePage->getNameSpace() != $nameSpace) { $this->_switchCodePage($nameSpace); } $this->_attributes = $_attributes; } /** * strip uri: from nameSpace * * @param unknown_type $_nameSpace * @return unknown */ protected function _stripNameSpace($_nameSpace) { return substr($_nameSpace, 4); } /** * get's called by xml parser when tag ends * * @param resource $_parser * @param string $_tag current tag prefixed with namespace */ protected function _handleEndTag($_parser, $_tag) { #echo "$_tag Level: $this->_level == $this->_nextStackPop \n"; if($this->_nextStackPop !== NULL && $this->_nextStackPop == $this->_level) { #echo "TAG: $_tag\n"; $this->_writeByte(Syncroton_Wbxml_Abstract::END); $subStream = $this->_stream; $subStreamLength = ftell($subStream); $this->_dtd = array_pop($this->_dtdStack); $this->_stream = array_pop($this->_streamStack); $this->_nextStackPop = array_pop($this->_popStack); $this->_codePage = $this->_dtd->getCurrentCodePage(); rewind($subStream); #while (!feof($subStream)) {$buffer = fgets($subStream, 4096);echo $buffer;} $this->_writeByte(Syncroton_Wbxml_Abstract::OPAQUE); $this->_writeMultibyteUInt($subStreamLength); $writenBytes = stream_copy_to_stream($subStream, $this->_stream); if($writenBytes !== $subStreamLength) { //echo "$writenBytes !== $subStreamLength\n"; throw new Syncroton_Wbxml_Exception('blow'); } fclose($subStream); #echo "$this->_nextStackPop \n"; exit; } else { if ($this->_currentTag !== NULL && $this->_currentTagData !== NULL) { $this->_writeTag($this->_currentTag, $this->_attributes, true, $this->_currentTagData); $this->_writeByte(Syncroton_Wbxml_Abstract::END); } elseif ($this->_currentTag !== NULL && $this->_currentTagData === NULL) { // for example tag with no data, jumps directly from _handleStartTag to _handleEndTag $this->_writeTag($this->_currentTag, $this->_attributes); // no end tag required, tag has no content } else { $this->_writeByte(Syncroton_Wbxml_Abstract::END); } } #list($urn, $tag) = explode(';', $_tag); echo " ($this->_level)\n"; // reset $this->_currentTag, as tag got writen to stream already $this->_currentTag = NULL; $this->_level--; } /** * collects data(value) of tag * can be called multiple lines if the value contains linebreaks * * @param resource $_parser the xml parser * @param string $_data the data(value) of the tag */ protected function _handleCharacters($_parser, $_data) { $this->_currentTagData .= $_data; } /** * writes tag with data to stream * * @param string $_tag * @param array $_attributes * @param bool $_hasContent * @param string $_data */ protected function _writeTag($_tag, $_attributes=NULL, $_hasContent=false, $_data=NULL) { if($_hasContent == false && $_data !== NULL) { throw new Syncroton_Wbxml_Exception('$_hasContent can not be false, when $_data !== NULL'); } // handle the tag $identity = $this->_codePage->getIdentity($_tag); if (is_array($_attributes) && isset($_attributes['uri:Syncroton;encoding'])) { $encoding = 'opaque'; unset($_attributes['uri:Syncroton;encoding']); } else { $encoding = 'termstring'; } if(!empty($_attributes)) { $identity |= 0x80; } if($_hasContent == true) { $identity |= 0x40; } $this->_writeByte($identity); // handle the data if($_data !== NULL) { if ($encoding == 'opaque') { $this->_writeOpaqueString(base64_decode($_data)); } else { $this->_writeTerminatedString($_data); } } $this->_currentTagData = NULL; } /** * switch code page * * @param string $_urn */ protected function _switchCodePage($_nameSpace) { try { $codePageName = $this->_stripNameSpace($_nameSpace); if(!defined('Syncroton_Wbxml_Dtd_ActiveSync::CODEPAGE_'. strtoupper($codePageName))) { throw new Syncroton_Wbxml_Exception('codepage ' . $codePageName . ' not found'); } // switch to another codepage // no need to write the wbxml header again $codePageId = constant('Syncroton_Wbxml_Dtd_ActiveSync::CODEPAGE_'. strtoupper($codePageName)); $this->_codePage = $this->_dtd->switchCodePage($codePageId); $this->_writeByte(Syncroton_Wbxml_Abstract::SWITCH_PAGE); $this->_writeByte($codePageId); } catch (Syncroton_Wbxml_Dtd_Exception_CodePageNotFound $e) { // switch to another dtd // need to write the wbxml header again // put old dtd and stream on stack $this->_dtdStack[] = $this->_dtd; $this->_streamStack[] = $this->_stream; $this->_popStack[] = $this->_nextStackPop; $this->_nextStackPop = $this->_level; $this->_stream = fopen("php://temp", 'r+'); $this->_initialize($_urn); } } } \ No newline at end of file