0e57ddd72a23efb874e834874ff32f2675549757
[tine20] / tine20 / library / Syncroton / lib / Syncroton / Server.php
1 <?php
2 /**
3  * Syncroton
4  *
5  * @package     Syncroton
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>
9  */
10
11 /**
12  * class to handle incoming http ActiveSync requests
13  * 
14  * @package     Syncroton
15  */
16 class Syncroton_Server
17 {
18     const PARAMETER_ATTACHMENTNAME = 0;
19     const PARAMETER_COLLECTIONID   = 1;
20     const PARAMETER_ITEMID         = 3;
21     const PARAMETER_OPTIONS        = 7;
22     
23     protected $_body;
24     
25     /**
26      * informations about the currently device
27      *
28      * @var Syncroton_Backend_IDevice
29      */
30     protected $_deviceBackend;
31     
32     /**
33      * @var Zend_Log
34      */
35     protected $_logger;
36     
37     /**
38      * @var Zend_Controller_Request_Http
39      */
40     protected $_request;
41     
42     protected $_userId;
43     
44     public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null)
45     {
46         if (Syncroton_Registry::isRegistered('loggerBackend')) {
47             $this->_logger = Syncroton_Registry::get('loggerBackend');
48         }
49         
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');
53         
54         $this->_deviceBackend = Syncroton_Registry::getDeviceBackend();
55         
56     }
57         
58     public function handle()
59     {
60         if ($this->_logger instanceof Zend_Log)
61             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod());
62         
63         switch($this->_request->getMethod()) {
64             case 'OPTIONS':
65                 $this->_handleOptions();
66                 break;
67         
68             case 'POST':
69                 $this->_handlePost();
70                 break;
71         
72             case 'GET':
73                 echo "It works!<br>Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}.";
74                 break;
75         }
76     }
77     
78     /**
79      * handle options request
80      */
81     protected function _handleOptions()
82     {
83         $command = new Syncroton_Command_Options();
84     
85         $this->_sendHeaders($command->getHeaders());
86     }
87     
88     protected function _sendHeaders(array $headers)
89     {
90         foreach ($headers as $name => $value) {
91             header($name . ': ' . $value);
92         }
93     } 
94     
95     /**
96      * handle post request
97      */
98     protected function _handlePost()
99     {
100         $requestParameters = $this->_getRequestParameters($this->_request);
101         
102         if ($this->_logger instanceof Zend_Log) 
103             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true));
104         
105         $className = 'Syncroton_Command_' . $requestParameters['command'];
106         
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
110             
111             header("HTTP/1.1 501 not implemented");
112             
113             return;
114         }
115         
116         // get user device
117         $device = $this->_getUserDevice($this->_userId, $requestParameters);
118         
119         if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') {
120             // decode wbxml request
121             try {
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());
127                 }
128             } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) {
129                 $requestBody = NULL;
130             }
131         } else {
132             $requestBody = $this->_body;
133         }
134         
135         header("MS-Server-ActiveSync: 14.00.0536.000");
136
137         try {
138             $command = new $className($requestBody, $device, $requestParameters);
139         
140             $command->handle();
141         
142             $response = $command->getResponse();
143             
144         } catch (Syncroton_Exception_ProvisioningNeeded $sepn) {
145             if ($this->_logger instanceof Zend_Log) 
146                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed");
147             
148             header("HTTP/1.1 449 Retry after sending a PROVISION command");
149             
150             if (version_compare($device->acsversion, '14.0', '>=')) {
151                 $response = $sepn->domDocument;
152             } else {
153                 // pre 14.0 method
154                 return;
155             }
156             
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());
164             
165             header("HTTP/1.1 500 Internal server error");
166             
167             return;
168         }
169         
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;
175             }
176             
177             if (isset($command) && $command instanceof Syncroton_Command_ICommand) {
178                 $this->_sendHeaders($command->getHeaders());
179             }
180             
181             $outputStream = fopen("php://temp", 'r+');
182             
183             $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3);
184             
185             try {
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());
191                 }
192                 
193                 header("HTTP/1.1 500 Internal server error");
194                 
195                 return;
196             }
197             
198             if ($requestParameters['acceptMultipart'] == true) {
199                 $parts = $command->getParts();
200                 
201                 // output multipartheader
202                 $bodyPartCount = 1 + count($parts);
203                 
204                 // number of parts (4 bytes)
205                 $header  = pack('i', $bodyPartCount);
206                 
207                 $partOffset = 4 + (($bodyPartCount * 2) * 4);
208                 
209                 // wbxml body start and length
210                 $streamStat = fstat($outputStream);
211                 $header .= pack('ii', $partOffset, $streamStat['size']);
212                 
213                 $partOffset += $streamStat['size'];
214                 
215                 // calculate start and length of parts
216                 foreach ($parts as $partId => $partStream) {
217                     rewind($partStream);
218                     $streamStat = fstat($partStream);
219                     
220                     // part start and length
221                     $header .= pack('ii', $partOffset, $streamStat['size']);
222                     $partOffset += $streamStat['size'];
223                 }
224                 
225                 echo $header;
226             }
227                         
228             // output body
229             rewind($outputStream);
230             fpassthru($outputStream);
231             
232             // output multiparts
233             if (isset($parts)) {
234                 foreach ($parts as $partStream) {
235                     rewind($partStream);
236                     fpassthru($partStream);
237                 }
238             }
239         }
240     }    
241     
242     /**
243      * return request params
244      * 
245      * @return array
246      */
247     protected function _getRequestParameters(Zend_Controller_Request_Http $request)
248     {
249         if (strpos($request->getRequestUri(), '&') === false) {
250             $commands = array(
251                 0  => 'Sync',
252                 1  => 'SendMail',
253                 2  => 'SmartForward',
254                 3  => 'SmartReply',
255                 4  => 'GetAttachment',
256                 9  => 'FolderSync',
257                 10 => 'FolderCreate',
258                 11 => 'FolderDelete',
259                 12 => 'FolderUpdate',
260                 13 => 'MoveItems',
261                 14 => 'GetItemEstimate',
262                 15 => 'MeetingResponse',
263                 16 => 'Search',
264                 17 => 'Settings',
265                 18 => 'Ping',
266                 19 => 'ItemOperations',
267                 20 => 'Provision',
268                 21 => 'ResolveRecipients',
269                 22 => 'ValidateCert'
270             );
271             
272             $requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?'));
273
274             $stream = fopen("php://temp", 'r+');
275             fwrite($stream, base64_decode($requestParameters));
276             rewind($stream);
277
278             // unpack the first 4 bytes
279             $unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4));
280             
281             // 140 => 14.0
282             $protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1);
283             $command = $commands[$unpacked['command']];
284             $locale = $unpacked['locale'];
285             
286             // unpack deviceId
287             $length = ord(fread($stream, 1));
288             if ($length > 0) {
289                 $toUnpack = fread($stream, $length);
290                 
291                 $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack);
292                 $deviceId = $unpacked['string'];
293             }
294             
295             // unpack policyKey
296             $length = ord(fread($stream, 1));
297             if ($length > 0) {
298                 $unpacked  = unpack('Vstring', fread($stream, $length));
299                 $policyKey = $unpacked['string'];
300             }\r
301             
302             // unpack device type\r
303             $length = ord(fread($stream, 1));
304             if ($length > 0) {\r
305                 $unpacked   = unpack('A' . $length . 'string', fread($stream, $length));
306                 $deviceType = $unpacked['string'];
307             }\r
308             
309             while (! feof($stream)) {
310                 $tag    = ord(fread($stream, 1));
311                 $length = ord(fread($stream, 1));
312
313                 switch ($tag) {
314                     case self::PARAMETER_ATTACHMENTNAME:
315                         $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
316                         
317                         $attachmentName = $unpacked['string'];
318                         break;
319                         
320                     case self::PARAMETER_COLLECTIONID:
321                         $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
322                         
323                         $collectionId = $unpacked['string'];
324                         break;
325                         
326                     case self::PARAMETER_ITEMID:
327                         $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
328                         
329                         $itemId = $unpacked['string'];
330                         break;
331                         
332                     case self::PARAMETER_OPTIONS:
333                         $options = ord(fread($stream, 1));
334                         
335                         $saveInSent      = !!($options & 0x01); 
336                         $acceptMultiPart = !!($options & 0x02);
337                         break;
338                         
339                     default:
340                         if ($this->_logger instanceof Zend_Log)
341                             $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters");
342                         
343                 }
344             }
345              
346             $result = array(
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
357             );
358         } else {\r
359             $result = array(
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
370             );
371         }
372         \r
373         $result['userAgent']   = $request->getServer('HTTP_USER_AGENT', $result['deviceType']);\r
374         $result['contentType'] = $request->getServer('CONTENT_TYPE');\r
375         
376         return $result;
377     }
378     
379     /**
380      * get existing device of owner or create new device for owner
381      *
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
388      */
389     protected function _getUserDevice($ownerId, $requestParameters)
390     {
391         try {
392             $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']);
393         
394             $device->useragent  = $requestParameters['userAgent'];
395             $device->acsversion = $requestParameters['protocolVersion'];
396             
397             if ($device->isDirty()) {
398                 $device = $this->_deviceBackend->update($device);
399             }
400         
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
409             )));
410         }
411
412         return $device;
413     }
414 }