6 * @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
7 * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
8 * @author Lars Kneschke <l.kneschke@metaways.de>
12 * class to handle incoming http ActiveSync requests
16 class Syncroton_Server
18 const PARAMETER_ATTACHMENTNAME = 0;
19 const PARAMETER_COLLECTIONID = 1;
20 const PARAMETER_ITEMID = 3;
21 const PARAMETER_OPTIONS = 7;
26 * informations about the currently device
28 * @var Syncroton_Backend_IDevice
30 protected $_deviceBackend;
38 * @var Zend_Controller_Request_Http
44 public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null)
46 if (Syncroton_Registry::isRegistered('loggerBackend')) {
47 $this->_logger = Syncroton_Registry::get('loggerBackend');
50 $this->_userId = $userId;
51 $this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http();
52 $this->_body = $body !== null ? $body : fopen('php://input', 'r');
54 $this->_deviceBackend = Syncroton_Registry::getDeviceBackend();
58 public function handle()
60 if ($this->_logger instanceof Zend_Log)
61 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod());
63 switch($this->_request->getMethod()) {
65 $this->_handleOptions();
73 echo "It works!<br>Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}.";
79 * handle options request
81 protected function _handleOptions()
83 $command = new Syncroton_Command_Options();
85 $this->_sendHeaders($command->getHeaders());
88 protected function _sendHeaders(array $headers)
90 foreach ($headers as $name => $value) {
91 header($name . ': ' . $value);
98 protected function _handlePost()
100 $requestParameters = $this->_getRequestParameters($this->_request);
102 if ($this->_logger instanceof Zend_Log)
103 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true));
105 $className = 'Syncroton_Command_' . $requestParameters['command'];
107 if(!class_exists($className)) {
108 if ($this->_logger instanceof Zend_Log)
\r
109 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']);
\r
111 header("HTTP/1.1 501 not implemented");
117 $device = $this->_getUserDevice($this->_userId, $requestParameters);
119 if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') {
120 // decode wbxml request
122 $decoder = new Syncroton_Wbxml_Decoder($this->_body);
123 $requestBody = $decoder->decode();
124 if ($this->_logger instanceof Zend_Log) {
125 $requestBody->formatOutput = true;
126 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML());
128 } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) {
132 $requestBody = $this->_body;
135 header("MS-Server-ActiveSync: 14.00.0536.000");
138 $command = new $className($requestBody, $device, $requestParameters);
142 $response = $command->getResponse();
144 } catch (Syncroton_Exception_ProvisioningNeeded $sepn) {
145 if ($this->_logger instanceof Zend_Log)
146 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed");
148 if (version_compare($device->acsversion, '14.0', '>=')) {
149 $response = $sepn->domDocument;
152 header("HTTP/1.1 449 Retry after sending a PROVISION command");
157 } catch (Exception $e) {
158 if ($this->_logger instanceof Zend_Log)
159 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
160 if ($this->_logger instanceof Zend_Log)
161 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage());
162 if ($this->_logger instanceof Zend_Log)
163 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
165 header("HTTP/1.1 500 Internal server error");
170 if ($response instanceof DOMDocument) {
171 if ($this->_logger instanceof Zend_Log) {
172 $response->formatOutput = true;
173 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml response:\n" . $response->saveXML());
174 $response->formatOutput = false;
177 if (isset($command) && $command instanceof Syncroton_Command_ICommand) {
178 $this->_sendHeaders($command->getHeaders());
181 $outputStream = fopen("php://temp", 'r+');
183 $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3);
186 $encoder->encode($response);
187 } catch (Syncroton_Wbxml_Exception $swe) {
188 if ($this->_logger instanceof Zend_Log) {
189 $this->_logger->err(__METHOD__ . '::' . __LINE__ . " Could not encode output: " . $swe);
190 $this->_logger->err(__METHOD__ . '::' . __LINE__ . " xml response:\n" . $response->saveXML());
193 header("HTTP/1.1 500 Internal server error");
198 if ($requestParameters['acceptMultipart'] == true) {
199 $parts = $command->getParts();
201 // output multipartheader
202 $bodyPartCount = 1 + count($parts);
204 // number of parts (4 bytes)
205 $header = pack('i', $bodyPartCount);
207 $partOffset = 4 + (($bodyPartCount * 2) * 4);
209 // wbxml body start and length
210 $streamStat = fstat($outputStream);
211 $header .= pack('ii', $partOffset, $streamStat['size']);
213 $partOffset += $streamStat['size'];
215 // calculate start and length of parts
216 foreach ($parts as $partId => $partStream) {
218 $streamStat = fstat($partStream);
220 // part start and length
221 $header .= pack('ii', $partOffset, $streamStat['size']);
222 $partOffset += $streamStat['size'];
229 rewind($outputStream);
230 fpassthru($outputStream);
234 foreach ($parts as $partStream) {
236 fpassthru($partStream);
243 * return request params
247 protected function _getRequestParameters(Zend_Controller_Request_Http $request)
249 if (strpos($request->getRequestUri(), '&') === false) {
255 4 => 'GetAttachment',
257 10 => 'FolderCreate',
258 11 => 'FolderDelete',
259 12 => 'FolderUpdate',
261 14 => 'GetItemEstimate',
262 15 => 'MeetingResponse',
266 19 => 'ItemOperations',
268 21 => 'ResolveRecipients',
272 $requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?'));
274 $stream = fopen("php://temp", 'r+');
275 fwrite($stream, base64_decode($requestParameters));
278 // unpack the first 4 bytes
279 $unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4));
282 $protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1);
283 $command = $commands[$unpacked['command']];
284 $locale = $unpacked['locale'];
287 $length = ord(fread($stream, 1));
289 $toUnpack = fread($stream, $length);
291 $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack);
292 $deviceId = $unpacked['string'];
296 $length = ord(fread($stream, 1));
298 $unpacked = unpack('Vstring', fread($stream, $length));
299 $policyKey = $unpacked['string'];
302 // unpack device type
\r
303 $length = ord(fread($stream, 1));
305 $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
306 $deviceType = $unpacked['string'];
309 while (! feof($stream)) {
310 $tag = ord(fread($stream, 1));
311 $length = ord(fread($stream, 1));
314 case self::PARAMETER_ATTACHMENTNAME:
315 $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
317 $attachmentName = $unpacked['string'];
320 case self::PARAMETER_COLLECTIONID:
321 $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
323 $collectionId = $unpacked['string'];
326 case self::PARAMETER_ITEMID:
327 $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
329 $itemId = $unpacked['string'];
332 case self::PARAMETER_OPTIONS:
333 $options = ord(fread($stream, 1));
335 $saveInSent = !!($options & 0x01);
336 $acceptMultiPart = !!($options & 0x02);
340 if ($this->_logger instanceof Zend_Log)
341 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters");
347 'protocolVersion' => $protocolVersion,
348 'command' => $command,
349 'deviceId' => $deviceId,
350 'deviceType' => isset($deviceType) ? $deviceType : null,
351 'policyKey' => isset($policyKey) ? $policyKey : null,
352 'saveInSent' => isset($saveInSent) ? $saveInSent : false,
353 'collectionId' => isset($collectionId) ? $collectionId : null,
354 'itemId' => isset($itemId) ? $itemId : null,
355 'attachmentName' => isset($attachmentName) ? $attachmentName : null,
356 'acceptMultipart' => isset($acceptMultiPart) ? $acceptMultiPart : false
360 'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'),
361 'command' => $request->getQuery('Cmd'),
362 'deviceId' => $request->getQuery('DeviceId'),
363 'deviceType' => $request->getQuery('DeviceType'),
364 'policyKey' => $request->getServer('HTTP_X_MS_POLICYKEY'),
365 'saveInSent' => $request->getQuery('SaveInSent') == 'T',
\r
366 'collectionId' => $request->getQuery('CollectionId'),
\r
367 'itemId' => $request->getQuery('ItemId'),
368 'attachmentName' => $request->getQuery('AttachmentName'),
369 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T'
\r
373 $result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']);
\r
374 $result['contentType'] = $request->getServer('CONTENT_TYPE');
\r
380 * get existing device of owner or create new device for owner
382 * @param unknown_type $ownerId
383 * @param unknown_type $deviceId
384 * @param unknown_type $deviceType
385 * @param unknown_type $userAgent
386 * @param unknown_type $protocolVersion
387 * @return Syncroton_Model_Device
389 protected function _getUserDevice($ownerId, $requestParameters)
392 $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']);
394 $device->useragent = $requestParameters['userAgent'];
395 $device->acsversion = $requestParameters['protocolVersion'];
397 if ($device->isDirty()) {
398 $device = $this->_deviceBackend->update($device);
401 } catch (Syncroton_Exception_NotFound $senf) {
402 $device = $this->_deviceBackend->create(new Syncroton_Model_Device(array(
403 'owner_id' => $ownerId,
404 'deviceid' => $requestParameters['deviceId'],
405 'devicetype' => $requestParameters['deviceType'],
406 'useragent' => $requestParameters['userAgent'],
407 'acsversion' => $requestParameters['protocolVersion'],
408 'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null
\r