Merge branch 'master' of http://git.syncroton.org/Syncroton
[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     protected $_body;
19     
20     /**
21      * informations about the currently device
22      *
23      * @var Syncroton_Backend_IDevice
24      */
25     protected $_deviceBackend;
26     
27     /**
28      * @var Zend_Log
29      */
30     protected $_logger;
31     
32     /**
33      * @var Zend_Controller_Request_Http
34      */
35     protected $_request;
36     
37     protected $_userId;
38     
39     public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null)
40     {
41         if (Syncroton_Registry::isRegistered('loggerBackend')) {
42             $this->_logger = Syncroton_Registry::get('loggerBackend');
43         }
44         
45         $this->_userId  = $userId;
46         $this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http();
47         $this->_body    = $body !== null ? $body : fopen('php://input', 'r');
48         
49         $this->_deviceBackend = Syncroton_Registry::getDeviceBackend();
50         
51     }
52         
53     public function handle()
54     {
55         if ($this->_logger instanceof Zend_Log)
56             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod());
57         
58         switch($this->_request->getMethod()) {
59             case 'OPTIONS':
60                 $this->_handleOptions();
61                 break;
62         
63             case 'POST':
64                 $this->_handlePost();
65                 break;
66         
67             case 'GET':
68                 echo "It works!<br>Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}.";
69                 break;
70         }
71     }
72     
73     /**
74     * handle options request
75     *
76     */
77     protected function _handleOptions()
78     {
79         $command = new Syncroton_Command_Options();
80     
81         $command->getResponse();
82     }
83     
84     protected function _handlePost()
85     {
86         $requestParameters = $this->_getRequestParameters($this->_request);
87         
88         if ($this->_logger instanceof Zend_Log) 
89             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true));
90         
91         $className = 'Syncroton_Command_' . $requestParameters['command'];
92         
93         if(!class_exists($className)) {
94             if ($this->_logger instanceof Zend_Log)\r
95                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']);\r
96             
97             header("HTTP/1.1 501 not implemented");
98             
99             return;
100         }
101         
102         // get user device
103         $device = $this->_getUserDevice($this->_userId, $requestParameters);
104         
105         if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml') {
106             // decode wbxml request
107             try {
108                 $decoder = new Syncroton_Wbxml_Decoder($this->_body);
109                 $requestBody = $decoder->decode();
110                 if ($this->_logger instanceof Zend_Log) {
111                     $requestBody->formatOutput = true;
112                     $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML());
113                 }
114             } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) {
115                 $requestBody = NULL;
116             }
117         } else {
118             $requestBody = $this->_body;
119         }
120         
121         if (PHP_SAPI !== 'cli') {
122             header("MS-Server-ActiveSync: 14.00.0536.000");
123         }
124
125         try {
126             $command = new $className($requestBody, $device, $requestParameters);
127         
128             $command->handle();
129         
130             $response = $command->getResponse();
131             
132         } catch (Syncroton_Exception_ProvisioningNeeded $sepn) {
133             if ($this->_logger instanceof Zend_Log) 
134                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed");
135             
136             if (version_compare($device->acsversion, '14.0', '>=')) {
137                 $response = $sepn->domDocument;
138             } else {
139                 // pre 14.0 method
140                 header("HTTP/1.1 449 Retry after sending a PROVISION command");
141                    
142                 return;
143             }
144             
145             
146             
147         } catch (Exception $e) {
148             if ($this->_logger instanceof Zend_Log)
149                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
150             if ($this->_logger instanceof Zend_Log)
151                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage());
152             if ($this->_logger instanceof Zend_Log)
153                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
154             
155             header("HTTP/1.1 500 Internal server error");
156             
157             return;
158         }
159         
160         if ($response instanceof DOMDocument) {
161             if ($this->_logger instanceof Zend_Log) {
162                 $response->formatOutput = true;
163                 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml response:\n" . $response->saveXML());
164             }
165         
166             $outputStream = fopen("php://temp", 'r+');
167         
168             $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3);
169             $encoder->encode($response);
170
171             // avoid sending headers while running on cli (phpunit)
172             if (PHP_SAPI !== 'cli') {
173                 header("Content-Type: application/vnd.ms-sync.wbxml");
174             }
175         
176             rewind($outputStream);
177             fpassthru($outputStream);
178         }
179     }
180     
181     /**
182      * return request params
183      * 
184      * @return array
185      */
186     protected function _getRequestParameters(Zend_Controller_Request_Http $request)
187     {
188         if(count($_GET) == 1) {\r
189             $arrayKeys = array_keys($_GET);
190             \r
191             $base64Decoded = base64_decode($arrayKeys[0]);
192             
193             $stream = fopen("php://temp", 'r+');
194             fwrite($stream, base64_decode($arrayKeys[0]));
195             rewind($stream);
196
197             #fpassthru($stream);rewind($stream);
198             
199             $protocolVersion = ord(fread($stream, 1));
200             
201             switch (ord(fread($stream, 1))) {
202                 case 0:
203                     $command = 'Sync';
204                     break;
205                 case 1:
206                     $command = 'SendMail';
207                     break;
208                 case 2:
209                     $command = 'SmartForward';
210                     break;
211                 case 3:
212                     $command = 'SmartReply';
213                     break;
214                 case 4:
215                     $command = 'GetAttachment';
216                     break;
217                 case 9:
218                     $command = 'FolderSync';
219                     break;
220                 case 10:
221                     $command = 'FolderCreate';
222                     break;
223                 case 11:
224                     $command = 'FolderDelete';
225                     break;
226                 case 12:
227                     $command = 'FolderUpdate';
228                     break;
229                 case 13:
230                     $command = 'MoveItems';
231                     break;
232                 case 14:
233                     $command = 'GetItemEstimate';
234                     break;
235                 case 15:
236                     $command = 'MeetingResponse';
237                     break;
238                 case 16:
239                     $command = 'Search';
240                     break;
241                 case 17:
242                     $command = 'Settings';
243                     break;
244                 case 18:
245                     $command = 'Ping';
246                     break;
247                 case 19:
248                     $command = 'ItemOperations';
249                     break;
250                 case 20:
251                     $command = 'Provision';
252                     break;
253                 case 21:
254                     $command = 'ResolveRecipients';
255                     break;
256                 case 22:
257                     $command = 'ValidateCert';
258                     break;
259             }
260             
261             $locale = fread($stream, 2);
262             
263             $deviceIdLength = ord(fread($stream, 1));
264             if ($deviceIdLength > 0) {
265                 $deviceId = fread($stream, $deviceIdLength);
266                 
267                 // some windows devices send a device name which contains 
268                 // special chars, which can't be stored in the database
269                 if (!ctype_alnum($deviceId)) {
270                     $deviceId = md5($deviceId);
271                 }
272             }
273             
274             $policyKeyLength = ord(fread($stream, 1));
275             if ($policyKeyLength > 0) {
276                 $policyKey = fread($stream, $policyKeyLength);
277             }\r
278             \r
279             $deviceTypeLength = ord(fread($stream, 1));
280             if ($deviceTypeLength > 0) {\r
281                 $deviceType = fread($stream, $deviceTypeLength);
282             }\r
283             
284             // @todo parse command parameters 
285             $result = array(
286                 'protocolVersion' => $protocolVersion,
287                 'command'         => $command,
288                 'deviceId'        => $deviceId,
289                 'deviceType'      => isset($deviceType) ? $deviceType : null,
290                 'policyKey'       => isset($policyKey)  ? $policyKey  : null,
291                 'saveInSent'      => null,
292                 'collectionId'    => null,
293                 'itemId'          => null,
294                 'attachmentName'  => null
295             );
296         } else {\r
297             $result = array(
298                 'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'),
299                 'command'         => $request->getQuery('Cmd'),
300                 'deviceId'        => $request->getQuery('DeviceId'),
301                 'deviceType'      => $request->getQuery('DeviceType'),
302                 'policyKey'       => $request->getServer('HTTP_X_MS_POLICYKEY'),
303                 'saveInSent'      => $request->getQuery('SaveInSent'),\r
304                 'collectionId'    => $request->getQuery('CollectionId'),\r
305                 'itemId'          => $request->getQuery('ItemId'),
306                 'attachmentName'  => $request->getQuery('AttachmentName'),\r
307             );
308         }
309         \r
310         $result['userAgent']   = $request->getServer('HTTP_USER_AGENT', $result['deviceType']);\r
311         $result['contentType'] = $request->getServer('CONTENT_TYPE');\r
312         
313         return $result;
314     }
315     
316     /**
317      * get existing device of owner or create new device for owner
318      * 
319      * @param unknown_type $ownerId
320      * @param unknown_type $deviceId
321      * @param unknown_type $deviceType
322      * @param unknown_type $userAgent
323      * @param unknown_type $protocolVersion
324      * @return Syncroton_Model_Device
325      */
326     protected function _getUserDevice($ownerId, $requestParameters)
327     {
328         try {
329             $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']);
330         
331             $device->useragent  = $requestParameters['userAgent'];
332             $device->acsversion = $requestParameters['protocolVersion'];
333             
334             $device = $this->_deviceBackend->update($device);
335         
336         } catch (Syncroton_Exception_NotFound $senf) {
337             $device = $this->_deviceBackend->create(new Syncroton_Model_Device(array(
338                 'owner_id'   => $ownerId,
339                 'deviceid'   => $requestParameters['deviceId'],
340                 'devicetype' => $requestParameters['deviceType'],
341                 'useragent'  => $requestParameters['userAgent'],
342                 'acsversion' => $requestParameters['protocolVersion'],
343                 'policyId'   => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null\r
344             )));
345         }
346         
347         return $device;
348     }
349 }