af19913565d976dc40b701ea4a4e8db04d451abe
[tine20] / tine20 / library / Syncope / lib / Syncope / Command / Sync.php
1 <?php
2 /**
3  * Syncope
4  *
5  * @package     Syncope
6  * @subpackage  Command
7  * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
8  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * class to handle ActiveSync Sync command
14  *
15  * @package     Syncope
16  * @subpackage  Command
17  */
18 class Syncope_Command_Sync extends Syncope_Command_Wbxml 
19 {
20     const STATUS_SUCCESS                                = 1;
21     const STATUS_PROTOCOL_VERSION_MISMATCH              = 2;
22     const STATUS_INVALID_SYNC_KEY                       = 3;
23     const STATUS_PROTOCOL_ERROR                         = 4;
24     const STATUS_SERVER_ERROR                           = 5;
25     const STATUS_ERROR_IN_CLIENT_SERVER_CONVERSION      = 6;
26     const STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT = 7;
27     const STATUS_OBJECT_NOT_FOUND                       = 8;
28     const STATUS_USER_ACCOUNT_MAYBE_OUT_OF_DISK_SPACE   = 9;
29     const STATUS_ERROR_SETTING_NOTIFICATION_GUID        = 10;
30     const STATUS_DEVICE_NOT_PROVISIONED_FOR_NOTIFICATIONS = 11;
31     const STATUS_FOLDER_HIERARCHY_HAS_CHANGED           = 12;
32     const STATUS_RESEND_FULL_XML                        = 13;
33     const STATUS_WAIT_INTERVAL_OUT_OF_RANGE             = 14;
34     
35     const CONFLICT_OVERWRITE_SERVER                     = 0;
36     const CONFLICT_OVERWRITE_PIM                        = 1;
37     
38     const MIMESUPPORT_DONT_SEND_MIME                    = 0;
39     const MIMESUPPORT_SMIME_ONLY                        = 1;
40     const MIMESUPPORT_SEND_MIME                         = 2;
41     
42     const BODY_TYPE_PLAIN_TEXT                          = 1;
43     const BODY_TYPE_HTML                                = 2;
44     const BODY_TYPE_RTF                                 = 3;
45     const BODY_TYPE_MIME                                = 4;
46     
47     /**
48      * truncate types
49      */
50     const TRUNCATE_ALL                                  = 0;
51     const TRUNCATE_4096                                 = 1;
52     const TRUNCATE_5120                                 = 2;
53     const TRUNCATE_7168                                 = 3;
54     const TRUNCATE_10240                                = 4;
55     const TRUNCATE_20480                                = 5;
56     const TRUNCATE_51200                                = 6;
57     const TRUNCATE_102400                               = 7;
58     const TRUNCATE_NOTHING                              = 8;
59
60     /**
61      * filter types
62      */
63     const FILTER_NOTHING        = 0;
64     const FILTER_1_DAY_BACK     = 1;
65     const FILTER_3_DAYS_BACK    = 2;
66     const FILTER_1_WEEK_BACK    = 3;
67     const FILTER_2_WEEKS_BACK   = 4;
68     const FILTER_1_MONTH_BACK   = 5;
69     const FILTER_3_MONTHS_BACK  = 6;
70     const FILTER_6_MONTHS_BACK  = 7;
71     const FILTER_INCOMPLETE     = 8;
72     
73
74     protected $_defaultNameSpace    = 'uri:AirSync';
75     protected $_documentElement     = 'Sync';
76     
77     /**
78      * list of collections
79      *
80      * @var array
81      */
82     protected $_collections = array();
83
84     /**
85      * total count of items in all collections
86      *
87      * @var integer
88      */
89     protected $_totalCount;
90     
91     /**
92      * there are more entries than WindowSize available
93      * the MoreAvailable tag hot added to the xml output
94      *
95      * @var boolean
96      */
97     protected $_moreAvailable = false;
98     
99     /**
100      * @var Syncope_Model_SyncState
101      */
102     protected $_syncState;
103     
104     /**
105      * process the XML file and add, change, delete or fetches data 
106      */
107     public function handle()
108     {
109         // input xml
110         $xml = simplexml_import_dom($this->_inputDom);
111         
112         foreach ($xml->Collections->Collection as $xmlCollection) {
113             $collectionData = array(
114                 'syncKey'         => (int)$xmlCollection->SyncKey,
115                 'class'           => isset($xmlCollection->Class) ? (string)$xmlCollection->Class : null,
116                 'collectionId'    => (string)$xmlCollection->CollectionId,
117                 'windowSize'      => isset($xmlCollection->WindowSize) ? (int)$xmlCollection->WindowSize : 100,
118                 'deletesAsMoves'  => isset($xmlCollection->DeletesAsMoves) ? true : false,
119                 'getChanges'      => isset($xmlCollection->GetChanges) ? true : false,
120                 'added'           => array(),
121                 'changed'         => array(),
122                 'deleted'         => array(),
123                 'forceAdd'        => array(),
124                 'forceChange'     => array(),
125                 'toBeFetched'     => array(),
126                 'filterType'      => 0,
127                 'mimeSupport'     => self::MIMESUPPORT_DONT_SEND_MIME,
128                 'mimeTruncation'  => Syncope_Command_Sync::TRUNCATE_NOTHING,
129                 'bodyPreferences' => array(),
130             );
131             
132             // process options
133             if (isset($xmlCollection->Options)) {
134                 // optional parameters
135                 if (isset($xmlCollection->Options->FilterType)) {
136                     $collectionData['filterType'] = (int)$xmlCollection->Options->FilterType;
137                 }
138                 if (isset($xmlCollection->Options->MIMESupport)) {
139                     $collectionData['mimeSupport'] = (int)$xmlCollection->Options->MIMESupport;
140                 }
141                 if (isset($xmlCollection->Options->MIMETruncation)) {
142                     $collectionData['mimeTruncation'] = (int)$xmlCollection->Options->MIMETruncation;
143                 }
144                 
145                 // try to fetch element from AirSyncBase:BodyPreference
146                 $airSyncBase = $xmlCollection->Options->children('uri:AirSyncBase');
147                 
148                 if (isset($airSyncBase->BodyPreference)) {
149                     
150                     foreach ($airSyncBase->BodyPreference as $bodyPreference) {
151                         $type = (int) $bodyPreference->Type;
152                         $collectionData['bodyPreferences'][$type] = array(
153                             'type' => $type
154                         );
155                         
156                         // optional
157                         if (isset($bodyPreference->TruncationSize)) {
158                             $collectionData['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
159                         }
160                     }
161                 }
162             }
163             
164             // got the folder synchronized to the device already
165             try {
166                 $collectionData['folder'] = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
167                 
168             } catch (Syncope_Exception_NotFound $senf) {
169                 if ($this->_logger instanceof Zend_Log) 
170                     $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData['collectionId']} not found");
171                 
172                 $this->_collections['invalidFolderId'][$collectionData['collectionId']] = $collectionData;
173                 continue;
174             }
175             
176             if ($this->_logger instanceof Zend_Log) 
177                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData['syncKey']} Class: {$collectionData['class']} CollectionId: {$collectionData['collectionId']}");
178             
179             // initial synckey
180             if($collectionData['syncKey'] === 0) {
181                 if ($this->_logger instanceof Zend_Log) 
182                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
183                 
184                 // reset sync state for this folder
185                 $this->_syncStateBackend->resetState($this->_device, $collectionData['folder']);
186                 $this->_contentStateBackend->resetState($this->_device, $collectionData['folder']);
187             
188                 $collectionData['syncState']    = new Syncope_Model_SyncState(array(
189                     'device_id' => $this->_device,
190                     'counter'   => 0,
191                     'type'      => $collectionData['folder'],
192                     'lastsync'  => $this->_syncTimeStamp
193                 ));
194                 
195                 $this->_collections[$collectionData['folder']->class][$collectionData['collectionId']] = $collectionData;
196                 
197                 continue;
198             }
199             
200             // check for invalid sycnkey
201             if(($collectionData['syncState'] = $this->_syncStateBackend->validate($this->_device, $collectionData['folder'], $collectionData['syncKey'])) === false) {
202                 if ($this->_logger instanceof Zend_Log) 
203                     $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData['syncKey']} provided");
204                 
205                 // reset sync state for this folder
206                 $this->_syncStateBackend->resetState($this->_device, $collectionData['folder']);
207                 $this->_contentStateBackend->resetState($this->_device, $collectionData['folder']);
208                 
209                 $this->_collections[$collectionData['folder']->class][$collectionData['collectionId']] = $collectionData;
210                 
211                 continue;
212             }
213             
214             $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class, $this->_device, $this->_syncTimeStamp);
215             
216             // handle incoming data
217             if(isset($xmlCollection->Commands->Add)) {
218                 $adds = $xmlCollection->Commands->Add;
219                 if ($this->_logger instanceof Zend_Log) 
220                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
221                 
222                 foreach ($adds as $add) {
223                     if ($this->_logger instanceof Zend_Log) 
224                         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
225
226                     try {
227                         if ($this->_logger instanceof Zend_Log) 
228                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . " adding entry as new");
229                         
230                         $serverId = $dataController->createEntry($collectionData['collectionId'], $add->ApplicationData);
231                         
232                         $collectionData['added'][$serverId] = array(
233                             'clientId' => (string)$add->ClientId,
234                             'serverId' => $serverId,
235                             'status'   => self::STATUS_SUCCESS
236                         );
237                         
238                         $this->_contentStateBackend->create(new Syncope_Model_Content(array(
239                             'device_id'     => $this->_device,
240                             'folder_id'     => $collectionData['folder'],
241                             'contentid'     => $serverId,
242                             'creation_time' => $this->_syncTimeStamp
243                         
244                         )));
245                     } catch (Exception $e) {
246                         if ($this->_logger instanceof Zend_Log) 
247                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
248                         $collectionData['added'][] = array(
249                             'clientId' => (string)$add->ClientId,
250                             'status'   => self::STATUS_SERVER_ERROR
251                         );
252                     }
253                 }
254             }
255             
256             // handle changes, but only if not first sync
257             if($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Change)) {
258                 $changes = $xmlCollection->Commands->Change;
259                 if ($this->_logger instanceof Zend_Log) 
260                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
261                 
262                 foreach ($changes as $change) {
263                     $serverId = (string)$change->ServerId;
264                     
265                     try {
266                         $dataController->updateEntry($collectionData['collectionId'], $serverId, $change->ApplicationData);
267                         $collectionData['changed'][$serverId] = self::STATUS_SUCCESS;
268                     } catch (Syncope_Exception_AccessDenied $e) {
269                         $collectionData['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
270                         $collectionData['forceChange'][$serverId] = $serverId;
271                     } catch (Syncope_Exception_NotFound $e) {
272                         // entry does not exist anymore, will get deleted automaticaly
273                         $collectionData['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
274                     } catch (Exception $e) {
275                         if ($this->_logger instanceof Zend_Log) 
276                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
277                         // something went wrong while trying to update the entry
278                         $collectionData['changed'][$serverId] = self::STATUS_SERVER_ERROR;
279                     }
280                 }
281             }
282             
283             // handle deletes, but only if not first sync
284             if(isset($xmlCollection->Commands->Delete)) {
285                 $deletes = $xmlCollection->Commands->Delete;
286                 if ($this->_logger instanceof Zend_Log) 
287                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
288                 
289                 foreach ($deletes as $delete) {
290                     $serverId = (string)$delete->ServerId;
291                     
292                     try {
293                         // check if we have sent this entry to the phone
294                         $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
295                         
296                         try {
297                             $dataController->deleteEntry($collectionData['collectionId'], $serverId, $collectionData);
298                         } catch(Syncope_Exception_NotFound $e) {
299                             if ($this->_logger instanceof Zend_Log) 
300                                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
301                         } catch (Syncope_Exception $e) {
302                             if ($this->_logger instanceof Zend_Log) 
303                                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
304                             $collectionData['forceAdd'][$serverId] = $serverId;
305                         }
306                         $this->_contentStateBackend->delete($state);
307                         
308                     } catch (Syncope_Exception_NotFound $senf) {
309                         if ($this->_logger instanceof Zend_Log) 
310                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
311                         // should we send a special status???
312                         //$collectionData['deleted'][$serverId] = self::STATUS_SUCCESS;
313                     }
314                     
315                     $collectionData['deleted'][$serverId] = self::STATUS_SUCCESS;
316                 }
317             }
318             
319             // handle fetches, but only if not first sync
320             if($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Fetch)) {
321                 // the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
322                 // unfortunately the iPhone dont set GetChanges to 0 when fetching email body, but is confused when we send
323                 // changes
324                 if (! isset($xmlCollection->GetChanges)) {
325                     $collectionData['getChanges'] = false;
326                 }
327                 
328                 $fetches = $xmlCollection->Commands->Fetch;
329                 if ($this->_logger instanceof Zend_Log) 
330                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
331                 foreach ($fetches as $fetch) {
332                     $serverId = (string)$fetch->ServerId;
333                     
334                     $collectionData['toBeFetched'][$serverId] = $serverId;
335                 }
336             }
337             
338             $this->_collections[$collectionData['folder']->class][$collectionData['collectionId']] = $collectionData;
339         }
340     }
341     
342     /**
343      * (non-PHPdoc)
344      * @see Syncope_Command_Wbxml::getResponse()
345      */
346     public function getResponse()
347     {
348         $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase' , 'uri:AirSyncBase');
349         
350         $sync = $this->_outputDom->documentElement;
351         
352         $collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections'));
353
354         foreach($this->_collections as $classCollections) {
355             foreach($classCollections as $collectionId => $collectionData) {
356                 
357                 // invalid collectionid provided
358                 if (empty($collectionData['folder'])) {
359                     $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
360                     if (!empty($collectionData['class'])) {
361                         $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
362                     }
363                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
364                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
365                     /**
366                      * i would expect to send STATUS_FOLDER_HIERARCHY_HAS_CHANGED but by reading the source code of Android I found out
367                      * that android triggers the FolderSync only on STATUS_OBJECT_NOT_FOUND
368                      */
369                     #$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
370                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
371
372                 // invalid synckey provided
373                 } elseif (! ($collectionData['syncState'] instanceof Syncope_Model_ISyncState)) {
374                     // set synckey to 0
375                     $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
376                     if (!empty($collectionData['class'])) {
377                         $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
378                     }
379                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
380                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
381                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
382                     
383
384                 // initial sync
385                 } elseif ($collectionData['syncState']->counter === 0) {
386                     $collectionData['syncState']->counter++;
387     
388                     // initial sync
389                     // send back a new SyncKey only
390                     $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
391                     if (!empty($collectionData['class'])) {
392                         $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
393                     }
394                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
395                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
396                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
397                     
398                 } else {
399
400                     $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class , $this->_device, $this->_syncTimeStamp);
401                     
402                     $serverAdds    = array();
403                     $serverChanges = array();
404                     $serverDeletes = array();
405                     $moreAvailable = false;
406                     
407                     if($collectionData['getChanges'] === true) {
408                         if($collectionData['syncKey'] === 1) {
409                             // get all available entries
410                             $serverAdds    = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
411                             
412                         } else {
413                             // continue sync session?
414                             if(is_array($collectionData['syncState']->pendingdata)) {
415                                 if ($this->_logger instanceof Zend_Log)
416                                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
417                                 
418                                 $serverAdds    = $collectionData['syncState']->pendingdata['serverAdds'];
419                                 $serverChanges = $collectionData['syncState']->pendingdata['serverChanges'];
420                                 $serverDeletes = $collectionData['syncState']->pendingdata['serverDeletes'];
421                             } else {
422                                 // fetch entries added since last sync
423                                 $allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData['folder']);
424                                 $allServerEntries = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
425                     
426                                 // add entries
427                                 $serverDiff = array_diff($allServerEntries, $allClientEntries);
428                                 // add entries which produced problems during delete from client
429                                 $serverAdds = $collectionData['forceAdd'];
430                                 // add entries not yet sent to client
431                                 $serverAdds = array_unique(array_merge($serverAdds, $serverDiff));
432                     
433                                 # @todo still needed?
434                                 foreach($serverAdds as $id => $serverId) {
435                                     // skip entries added by client during this sync session
436                                     if(isset($collectionData['added'][$serverId]) && !isset($collectionData['forceAdd'][$serverId])) {
437                                         if ($this->_logger instanceof Zend_Log)
438                                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
439                                         unset($serverAdds[$id]);
440                                     }
441                                 }
442                     
443                                 // entries to be deleted
444                                 $serverDeletes = array_diff($allClientEntries, $allServerEntries);
445                     
446                                 // fetch entries changed since last sync
447                                 $serverChanges = $dataController->getChangedEntries($collectionData['collectionId'], $collectionData['syncState']->lastsync, $this->_syncTimeStamp);
448                                 $serverChanges = array_merge($serverChanges, $collectionData['forceChange']);
449                     
450                                 foreach($serverChanges as $id => $serverId) {
451                                     // skip entry, if it got changed by client during current sync
452                                     if(isset($collectionData['changed'][$serverId]) && !isset($collectionData['forceChange'][$serverId])) {
453                                         if ($this->_logger instanceof Zend_Log)
454                                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
455                                         unset($serverChanges[$id]);
456                                     }
457                                 }
458                     
459                                 // entries comeing in scope are already in $serverAdds and do not need to
460                                 // be send with $serverCanges
461                                 $serverChanges = array_diff($serverChanges, $serverAdds);
462                             }
463                         }
464                     
465                         if ($this->_logger instanceof Zend_Log)
466                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverAdds) . '/' . count($serverChanges) . '/' . count($serverDeletes)  . ' entries for sync from server to client');
467                     }
468                     
469                     
470                     
471                     
472                     if (!empty($collectionData['added']) || !empty($collectionData['changed']) || !empty($collectionData['deleted']) ||
473                         !empty($serverAdds) || !empty($serverChanges) || !empty($serverDeletes)) {
474                         $collectionData['syncState']->counter++;
475                     }
476                     
477                     // collection header
478                     $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
479                     if (!empty($collectionData['class'])) {
480                         $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
481                     }
482                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
483                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
484                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
485                     
486                     $responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
487                     
488                     // send reponse for newly added entries
489                     if(!empty($collectionData['added'])) {
490                         foreach($collectionData['added'] as $entryData) {
491                             $add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
492                             $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
493                             // we have no serverId is the add failed
494                             if(isset($entryData['serverId'])) {
495                                 $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
496                             }
497                             $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
498                         }
499                     }
500                     
501                     // send reponse for changed entries
502                     if(!empty($collectionData['changed'])) {
503                         foreach($collectionData['changed'] as $serverId => $status) {
504                             if ($status !== Syncope_Command_Sync::STATUS_SUCCESS) {
505                                 $change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
506                                 $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
507                                 $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
508                             }
509                         }
510                     }
511                     
512                     // send response for to be fetched entries
513                     if(!empty($collectionData['toBeFetched'])) {
514                         foreach($collectionData['toBeFetched'] as $serverId) {
515                             $fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
516                             $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
517                             
518                             try {
519                                 $applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
520                                 $dataController->appendXML($applicationData, $collectionData, $serverId);
521                                 
522                                 $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
523                                 
524                                 $fetch->appendChild($applicationData);
525                             } catch (Exception $e) {
526                                 if ($this->_logger instanceof Zend_Log) 
527                                     $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
528                                 $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
529                             }
530                         }
531                     }
532                     
533                     if ($responses->hasChildNodes() === true) {
534                         $collection->appendChild($responses);
535                     }
536                     
537                     if ((count($serverAdds) + count($serverChanges) + count($serverDeletes)) > $collectionData['windowSize'] ) {
538                         $moreAvailable = true;
539                         $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
540                     }
541                     
542                     $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
543                     
544                     
545                     /**
546                      * process entries added on server side
547                      */
548                     $newContentStates = array();
549                     
550                     foreach($serverAdds as $id => $serverId) {
551                         if($this->_totalCount === $collectionData['windowSize']) {
552                             break;
553                         }
554                         
555                         /**
556                          * somewhere is a problem in the logic for handling moreAvailable
557                          * 
558                          * it can happen, that we have a contentstate (which means we sent the entry to the client
559                          * and that this entry is yet in $collectionData['syncState']->pendingdata['serverAdds']
560                          * I have no idea how this can happen, but the next lines of code work around this problem
561                          */
562                         #try {
563                         #    $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
564                         #
565                         #    if ($this->_logger instanceof Zend_Log) 
566                         #        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client");
567                         #    
568                         #    unset($serverAdds[$id]);
569                         #    continue;
570                         #    
571                         #} catch (Syncope_Exception_NotFound $senf) {
572                         #    // do nothing => content state should not exist yet
573                         #}
574                         
575                         try {
576                             $add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
577                             $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
578                             
579                             $applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
580                             $dataController->appendXML($applicationData, $collectionData, $serverId);
581     
582                             $commands->appendChild($add);
583                             
584                             $this->_totalCount++;
585                         } catch (Exception $e) {
586                             if ($this->_logger instanceof Zend_Log) 
587                                 $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
588                         }
589                         
590                         // mark as send to the client, even the conversion to xml might have failed 
591                         $newContentStates[] = new Syncope_Model_Content(array(
592                             'device_id'     => $this->_device,
593                             'folder_id'     => $collectionData['folder'],
594                             'contentid'     => $serverId,
595                             'creation_time' => $this->_syncTimeStamp
596                         ));
597                         unset($serverAdds[$id]);    
598                     }
599
600                     /**
601                      * process entries changed on server side
602                      */
603                     foreach($serverChanges as $id => $serverId) {
604                         if($this->_totalCount === $collectionData['windowSize']) {
605                             break;
606                         }
607
608                         try {
609                             $change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
610                             $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
611                             
612                             $applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
613                             $dataController->appendXML($applicationData, $collectionData, $serverId);
614     
615                             $commands->appendChild($change);
616                             
617                             $this->_totalCount++;
618                         } catch (Exception $e) {
619                             if ($this->_logger instanceof Zend_Log) 
620                                 $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
621                         }
622
623                         unset($serverChanges[$id]);    
624                     }
625
626                     /**
627                      * process entries deleted on server side
628                      */
629                     $deletedContentStates = array();
630                     
631                     foreach($serverDeletes as $id => $serverId) {
632                         if($this->_totalCount === $collectionData['windowSize']) {
633                             break;
634                         }
635                                                     
636                         try {
637                             // check if we have sent this entry to the phone
638                             $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
639                             
640                             $delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
641                             $delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
642                             
643                             $deletedContentStates[] = $state;
644                             
645                             $commands->appendChild($delete);
646                             
647                             $this->_totalCount++;
648                         } catch (Exception $e) {
649                             if ($this->_logger instanceof Zend_Log) 
650                                 $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
651                         }
652                         
653                         unset($serverDeletes[$id]);    
654                     }
655                     
656                     if ($commands->hasChildNodes() === true) {
657                         $collection->appendChild($commands);
658                     }
659                     
660                     if ($this->_logger instanceof Zend_Log) 
661                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData['syncState']->counter);                
662                 }
663                 
664                 if (isset($collectionData['syncState']) && $collectionData['syncState'] instanceof Syncope_Model_ISyncState && 
665                     $collectionData['syncState']->counter != $collectionData['syncKey']) {
666                     // increment sync timestamp by 1 second
667                     $this->_syncTimeStamp->modify('+1 sec');
668                     
669                     // store pending data in sync state when needed
670                     if(isset($moreAvailable) && $moreAvailable === true) {
671                         $collectionData['syncState']->pendingdata = array(
672                             'serverAdds'    => (array)$serverAdds,
673                             'serverChanges' => (array)$serverChanges,
674                             'serverDeletes' => (array)$serverDeletes
675                         );
676                     } else {
677                         $collectionData['syncState']->pendingdata = null;
678                     }
679                     
680                     
681                     if (!empty($collectionData['added'])) {
682                         if ($this->_logger instanceof Zend_Log) 
683                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
684                         $keepPreviousSyncKey = false;
685                     } else {
686                         $keepPreviousSyncKey = true;
687                     }
688                     
689                     $collectionData['syncState']->lastsync = $this->_syncTimeStamp;
690                     
691                     // store new synckey
692                     $this->_syncStateBackend->create($collectionData['syncState'], $keepPreviousSyncKey);
693                     
694                     // store contentstates for new entries
695                     if (isset($newContentStates)) {
696                         foreach($newContentStates as $state) {
697                             $this->_contentStateBackend->create($state);
698                         }
699                     }
700                     
701                     // removed contentstates for deleted entries
702                     if (isset($deletedContentStates)) {
703                         foreach($deletedContentStates as $state) {
704                             $this->_contentStateBackend->delete($state);
705                         }
706                     }
707                     
708                     // store current filter type
709                     try {
710                         $folderState = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
711                         $folderState->lastfiltertype = $collectionData['filterType'];
712                         $this->_folderBackend->update($folderState);
713                     } catch (Syncope_Exception_NotFound $senf) {
714                         // failed to get folderstate => should not happen but is also no problem in this state
715                         if ($this->_logger instanceof Zend_Log) 
716                             $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData['collectionId']);
717                     }
718                 }
719             }
720         }
721         
722         return $this->_outputDom;
723     }
724 }