Merge branch 'master' of http://git.tine20.org/git/Syncope
[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             $collectionData['syncState'] = null;
165             $collectionData['folder']    = null;
166             
167             // got the folder synchronized to the device already
168             try {
169                 $collectionData['folder'] = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
170                 
171             } catch (Syncope_Exception_NotFound $senf) {
172                 if ($this->_logger instanceof Zend_Log) 
173                     $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData['collectionId']} not found");
174                 
175                 // trigger INVALID_SYNCKEY instead of OBJECT_NOTFOUND when synckey is bigger than 0
176                 // to avoid a syncloop for the iPhone
177                 if ($collectionData['syncKey'] > 0) {
178                     $collectionData['folder']    = new Syncope_Model_Folder(array(
179                         'device_id' => $this->_device,
180                         'folderid'  => $collectionData['collectionId']
181                     ));
182                 }
183                 
184                 $this->_collections[] = $collectionData;
185                 continue;
186             }
187             
188             if ($this->_logger instanceof Zend_Log) 
189                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData['syncKey']} Class: {$collectionData['folder']->class} CollectionId: {$collectionData['collectionId']}");
190             
191             // initial synckey
192             if($collectionData['syncKey'] === 0) {
193                 if ($this->_logger instanceof Zend_Log) 
194                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
195                 
196                 // reset sync state for this folder
197                 $this->_syncStateBackend->resetState($this->_device, $collectionData['folder']);
198                 $this->_contentStateBackend->resetState($this->_device, $collectionData['folder']);
199             
200                 $collectionData['syncState']    = new Syncope_Model_SyncState(array(
201                     'device_id' => $this->_device,
202                     'counter'   => 0,
203                     'type'      => $collectionData['folder'],
204                     'lastsync'  => $this->_syncTimeStamp
205                 ));
206                 
207                 $this->_collections[] = $collectionData;
208                 
209                 continue;
210             }
211             
212             // check for invalid sycnkey
213             if(($collectionData['syncState'] = $this->_syncStateBackend->validate($this->_device, $collectionData['folder'], $collectionData['syncKey'])) === false) {
214                 if ($this->_logger instanceof Zend_Log) 
215                     $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData['syncKey']} provided");
216                 
217                 // reset sync state for this folder
218                 $this->_syncStateBackend->resetState($this->_device, $collectionData['folder']);
219                 $this->_contentStateBackend->resetState($this->_device, $collectionData['folder']);
220                 
221                 $this->_collections[] = $collectionData;
222                 
223                 continue;
224             }
225             
226             $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class, $this->_device, $this->_syncTimeStamp);
227             
228             // handle incoming data
229             if(isset($xmlCollection->Commands->Add)) {
230                 $adds = $xmlCollection->Commands->Add;
231                 if ($this->_logger instanceof Zend_Log) 
232                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
233                 
234                 foreach ($adds as $add) {
235                     if ($this->_logger instanceof Zend_Log) 
236                         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
237
238                     try {
239                         if ($this->_logger instanceof Zend_Log) 
240                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . " adding entry as new");
241                         
242                         $serverId = $dataController->createEntry($collectionData['collectionId'], $add->ApplicationData);
243                         
244                         $collectionData['added'][$serverId] = array(
245                             'clientId'     => (string)$add->ClientId,
246                             'serverId'     => $serverId,
247                             'status'       => self::STATUS_SUCCESS,
248                             'contentState' => $this->_contentStateBackend->create(new Syncope_Model_Content(array(
249                                 'device_id'        => $this->_device,
250                                 'folder_id'        => $collectionData['folder'],
251                                 'contentid'        => $serverId,
252                                 'creation_time'    => $this->_syncTimeStamp,
253                                 'creation_synckey' => $collectionData['syncKey'] + 1
254                             )))
255                         );
256                         
257                         
258                     } catch (Exception $e) {
259                         if ($this->_logger instanceof Zend_Log) 
260                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
261                         $collectionData['added'][] = array(
262                             'clientId' => (string)$add->ClientId,
263                             'status'   => self::STATUS_SERVER_ERROR
264                         );
265                     }
266                 }
267             }
268             
269             // handle changes, but only if not first sync
270             if($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Change)) {
271                 $changes = $xmlCollection->Commands->Change;
272                 if ($this->_logger instanceof Zend_Log) 
273                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
274                 
275                 foreach ($changes as $change) {
276                     $serverId = (string)$change->ServerId;
277                     
278                     try {
279                         $dataController->updateEntry($collectionData['collectionId'], $serverId, $change->ApplicationData);
280                         $collectionData['changed'][$serverId] = self::STATUS_SUCCESS;
281                     } catch (Syncope_Exception_AccessDenied $e) {
282                         $collectionData['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
283                         $collectionData['forceChange'][$serverId] = $serverId;
284                     } catch (Syncope_Exception_NotFound $e) {
285                         // entry does not exist anymore, will get deleted automaticaly
286                         $collectionData['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
287                     } catch (Exception $e) {
288                         if ($this->_logger instanceof Zend_Log) 
289                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
290                         // something went wrong while trying to update the entry
291                         $collectionData['changed'][$serverId] = self::STATUS_SERVER_ERROR;
292                     }
293                 }
294             }
295             
296             // handle deletes, but only if not first sync
297             if(isset($xmlCollection->Commands->Delete)) {
298                 $deletes = $xmlCollection->Commands->Delete;
299                 if ($this->_logger instanceof Zend_Log) 
300                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
301                 
302                 foreach ($deletes as $delete) {
303                     $serverId = (string)$delete->ServerId;
304                     
305                     try {
306                         // check if we have sent this entry to the phone
307                         $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
308                         
309                         try {
310                             $dataController->deleteEntry($collectionData['collectionId'], $serverId, $collectionData);
311                         } catch(Syncope_Exception_NotFound $e) {
312                             if ($this->_logger instanceof Zend_Log) 
313                                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
314                         } catch (Syncope_Exception $e) {
315                             if ($this->_logger instanceof Zend_Log) 
316                                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
317                             $collectionData['forceAdd'][$serverId] = $serverId;
318                         }
319                         $this->_contentStateBackend->delete($state);
320                         
321                     } catch (Syncope_Exception_NotFound $senf) {
322                         if ($this->_logger instanceof Zend_Log) 
323                             $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
324                         // should we send a special status???
325                         //$collectionData['deleted'][$serverId] = self::STATUS_SUCCESS;
326                     }
327                     
328                     $collectionData['deleted'][$serverId] = self::STATUS_SUCCESS;
329                 }
330             }
331             
332             // handle fetches, but only if not first sync
333             if($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Fetch)) {
334                 // the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
335                 // unfortunately the iPhone dont set GetChanges to 0 when fetching email body, but is confused when we send
336                 // changes
337                 if (! isset($xmlCollection->GetChanges)) {
338                     $collectionData['getChanges'] = false;
339                 }
340                 
341                 $fetches = $xmlCollection->Commands->Fetch;
342                 if ($this->_logger instanceof Zend_Log) 
343                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
344                 foreach ($fetches as $fetch) {
345                     $serverId = (string)$fetch->ServerId;
346                     
347                     $collectionData['toBeFetched'][$serverId] = $serverId;
348                 }
349             }
350             
351             $this->_collections[] = $collectionData;
352         }
353     }
354     
355     /**
356      * (non-PHPdoc)
357      * @see Syncope_Command_Wbxml::getResponse()
358      */
359     public function getResponse()
360     {
361         $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase' , 'uri:AirSyncBase');
362         
363         $sync = $this->_outputDom->documentElement;
364         
365         $collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections'));
366
367         foreach($this->_collections as $collectionData) {
368             // invalid collectionid provided
369             if (! ($collectionData['folder'] instanceof Syncope_Model_IFolder)) {
370                 $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
371                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
372                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
373                 /**
374                  * i would expect to send STATUS_FOLDER_HIERARCHY_HAS_CHANGED but by reading the source code of Android I found out
375                  * that android triggers the FolderSync only on STATUS_OBJECT_NOT_FOUND
376                  */
377                 #$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
378                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
379
380             // invalid synckey provided
381             } elseif (! ($collectionData['syncState'] instanceof Syncope_Model_ISyncState)) {
382                 // set synckey to 0
383                 $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
384                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
385                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
386                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
387                 
388
389             // initial sync
390             } elseif ($collectionData['syncState']->counter === 0) {
391                 $collectionData['syncState']->counter++;
392
393                 // initial sync
394                 // send back a new SyncKey only
395                 $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
396                 if (!empty($collectionData['folder']->class)) {
397                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['folder']->class));
398                 }
399                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
400                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
401                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
402                 
403             } else {
404
405                 $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class , $this->_device, $this->_syncTimeStamp);
406                 
407                 $serverAdds    = array();
408                 $serverChanges = array();
409                 $serverDeletes = array();
410                 $moreAvailable = false;
411                 
412                 if($collectionData['getChanges'] === true) {
413                     if($collectionData['syncKey'] === 1) {
414                         // get all available entries
415                         $serverAdds    = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
416                         
417                     } else {
418                         // continue sync session?
419                         if(is_array($collectionData['syncState']->pendingdata)) {
420                             if ($this->_logger instanceof Zend_Log)
421                                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
422                             
423                             $serverAdds    = $collectionData['syncState']->pendingdata['serverAdds'];
424                             $serverChanges = $collectionData['syncState']->pendingdata['serverChanges'];
425                             $serverDeletes = $collectionData['syncState']->pendingdata['serverDeletes'];
426                         } else {
427                             // fetch entries added since last sync
428                             $allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData['folder']);
429                             $allServerEntries = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
430                 
431                             // add entries
432                             $serverDiff = array_diff($allServerEntries, $allClientEntries);
433                             // add entries which produced problems during delete from client
434                             $serverAdds = $collectionData['forceAdd'];
435                             // add entries not yet sent to client
436                             $serverAdds = array_unique(array_merge($serverAdds, $serverDiff));
437                 
438                             # @todo still needed?
439                             foreach($serverAdds as $id => $serverId) {
440                                 // skip entries added by client during this sync session
441                                 if(isset($collectionData['added'][$serverId]) && !isset($collectionData['forceAdd'][$serverId])) {
442                                     if ($this->_logger instanceof Zend_Log)
443                                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
444                                     unset($serverAdds[$id]);
445                                 }
446                             }
447                 
448                             // entries to be deleted
449                             $serverDeletes = array_diff($allClientEntries, $allServerEntries);
450                 
451                             // fetch entries changed since last sync
452                             $serverChanges = $dataController->getChangedEntries($collectionData['collectionId'], $collectionData['syncState']->lastsync, $this->_syncTimeStamp);
453                             $serverChanges = array_merge($serverChanges, $collectionData['forceChange']);
454                 
455                             foreach($serverChanges as $id => $serverId) {
456                                 // skip entry, if it got changed by client during current sync
457                                 if(isset($collectionData['changed'][$serverId]) && !isset($collectionData['forceChange'][$serverId])) {
458                                     if ($this->_logger instanceof Zend_Log)
459                                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
460                                     unset($serverChanges[$id]);
461                                 }
462                             }
463                 
464                             // entries comeing in scope are already in $serverAdds and do not need to
465                             // be send with $serverCanges
466                             $serverChanges = array_diff($serverChanges, $serverAdds);
467                         }
468                     }
469                 
470                     if ($this->_logger instanceof Zend_Log)
471                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverAdds) . '/' . count($serverChanges) . '/' . count($serverDeletes)  . ' entries for sync from server to client');
472                 }
473                 
474                 
475                 
476                 
477                 if (!empty($collectionData['added']) || !empty($collectionData['changed']) || !empty($collectionData['deleted']) ||
478                     !empty($serverAdds) || !empty($serverChanges) || !empty($serverDeletes)) {
479                     $collectionData['syncState']->counter++;
480                 }
481                 
482                 // collection header
483                 $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
484                 if (!empty($collectionData['folder']->class)) {
485                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['folder']->class));
486                 }
487                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
488                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
489                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
490                 
491                 $responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
492                 
493                 // send reponse for newly added entries
494                 if(!empty($collectionData['added'])) {
495                     foreach($collectionData['added'] as $entryData) {
496                         $add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
497                         $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
498                         // we have no serverId is the add failed
499                         if(isset($entryData['serverId'])) {
500                             $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
501                         }
502                         $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
503                     }
504                 }
505                 
506                 // send reponse for changed entries
507                 if(!empty($collectionData['changed'])) {
508                     foreach($collectionData['changed'] as $serverId => $status) {
509                         if ($status !== Syncope_Command_Sync::STATUS_SUCCESS) {
510                             $change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
511                             $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
512                             $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
513                         }
514                     }
515                 }
516                 
517                 // send response for to be fetched entries
518                 if(!empty($collectionData['toBeFetched'])) {
519                     foreach($collectionData['toBeFetched'] as $serverId) {
520                         $fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
521                         $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
522                         
523                         try {
524                             $applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
525                             $dataController->appendXML($applicationData, $collectionData, $serverId);
526                             
527                             $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
528                             
529                             $fetch->appendChild($applicationData);
530                         } catch (Exception $e) {
531                             if ($this->_logger instanceof Zend_Log) 
532                                 $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
533                             $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
534                         }
535                     }
536                 }
537                 
538                 if ($responses->hasChildNodes() === true) {
539                     $collection->appendChild($responses);
540                 }
541                 
542                 if ((count($serverAdds) + count($serverChanges) + count($serverDeletes)) > $collectionData['windowSize'] ) {
543                     $moreAvailable = true;
544                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
545                 }
546                 
547                 $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
548                 
549                 
550                 /**
551                  * process entries added on server side
552                  */
553                 $newContentStates = array();
554                 
555                 foreach($serverAdds as $id => $serverId) {
556                     if($this->_totalCount === $collectionData['windowSize']) {
557                         break;
558                     }
559                     
560                     #/**
561                     # * somewhere is a problem in the logic for handling moreAvailable
562                     # * 
563                     # * it can happen, that we have a contentstate (which means we sent the entry to the client
564                     # * and that this entry is yet in $collectionData['syncState']->pendingdata['serverAdds']
565                     # * I have no idea how this can happen, but the next lines of code work around this problem
566                     # */
567                     #try {
568                     #    $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
569                     # 
570                     #    if ($this->_logger instanceof Zend_Log) 
571                     #        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client");
572                     #    
573                     #    unset($serverAdds[$id]);
574                     #    continue;
575                     #    
576                     #} catch (Syncope_Exception_NotFound $senf) {
577                     #    // do nothing => content state should not exist yet
578                     #}
579                     
580                     try {
581                         $add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
582                         $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
583                         
584                         $applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
585                         $dataController->appendXML($applicationData, $collectionData, $serverId);
586
587                         $commands->appendChild($add);
588                         
589                         $this->_totalCount++;
590                     } catch (Exception $e) {
591                         if ($this->_logger instanceof Zend_Log) 
592                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
593                     }
594                     
595                     // mark as send to the client, even the conversion to xml might have failed 
596                     $newContentStates[] = new Syncope_Model_Content(array(
597                         'device_id'        => $this->_device,
598                         'folder_id'        => $collectionData['folder'],
599                         'contentid'        => $serverId,
600                         'creation_time'    => $this->_syncTimeStamp,
601                         'creation_synckey' => $collectionData['syncState']->counter
602                     ));
603                     unset($serverAdds[$id]);    
604                 }
605
606                 /**
607                  * process entries changed on server side
608                  */
609                 foreach($serverChanges as $id => $serverId) {
610                     if($this->_totalCount === $collectionData['windowSize']) {
611                         break;
612                     }
613
614                     try {
615                         $change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
616                         $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
617                         
618                         $applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
619                         $dataController->appendXML($applicationData, $collectionData, $serverId);
620
621                         $commands->appendChild($change);
622                         
623                         $this->_totalCount++;
624                     } catch (Exception $e) {
625                         if ($this->_logger instanceof Zend_Log) 
626                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
627                     }
628
629                     unset($serverChanges[$id]);    
630                 }
631
632                 /**
633                  * process entries deleted on server side
634                  */
635                 $deletedContentStates = array();
636                 
637                 foreach($serverDeletes as $id => $serverId) {
638                     if($this->_totalCount === $collectionData['windowSize']) {
639                         break;
640                     }
641                                                 
642                     try {
643                         // check if we have sent this entry to the phone
644                         $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
645                         
646                         $delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
647                         $delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
648                         
649                         $deletedContentStates[] = $state;
650                         
651                         $commands->appendChild($delete);
652                         
653                         $this->_totalCount++;
654                     } catch (Exception $e) {
655                         if ($this->_logger instanceof Zend_Log) 
656                             $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
657                     }
658                     
659                     unset($serverDeletes[$id]);    
660                 }
661                 
662                 if ($commands->hasChildNodes() === true) {
663                     $collection->appendChild($commands);
664                 }
665                 
666                 if ($this->_logger instanceof Zend_Log) 
667                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData['syncState']->counter);                
668             }
669             
670             if (isset($collectionData['syncState']) && $collectionData['syncState'] instanceof Syncope_Model_ISyncState && 
671                 $collectionData['syncState']->counter != $collectionData['syncKey']) {
672                         
673                 // increment sync timestamp by 1 second
674                 $this->_syncTimeStamp->modify('+1 sec');
675                 
676                 // store pending data in sync state when needed
677                 if(isset($moreAvailable) && $moreAvailable === true) {
678                     $collectionData['syncState']->pendingdata = array(
679                         'serverAdds'    => (array)$serverAdds,
680                         'serverChanges' => (array)$serverChanges,
681                         'serverDeletes' => (array)$serverDeletes
682                     );
683                 } else {
684                     $collectionData['syncState']->pendingdata = null;
685                 }
686                 
687                 
688                 if (!empty($collectionData['added'])) {
689                     if ($this->_logger instanceof Zend_Log) 
690                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
691                     $keepPreviousSyncKey = false;
692                 } else {
693                     $keepPreviousSyncKey = true;
694                 }
695                 
696                 $collectionData['syncState']->lastsync = $this->_syncTimeStamp;
697                 
698                 try {
699                     $transactionId = Syncope_Registry::getTransactionManager()->startTransaction(Syncope_Registry::getDatabase());
700                     
701                     // store new synckey
702                     $this->_syncStateBackend->create($collectionData['syncState'], $keepPreviousSyncKey);
703                     
704                     // store contentstates for new entries added to client
705                     if (isset($newContentStates)) {
706                         foreach($newContentStates as $state) {
707                             $this->_contentStateBackend->create($state);
708                         }
709                     }
710                     
711                     // remove contentstates for entries to be deleted on client
712                     if (isset($deletedContentStates)) {
713                         foreach($deletedContentStates as $state) {
714                             $this->_contentStateBackend->delete($state);
715                         }
716                     }
717                     
718                     Syncope_Registry::getTransactionManager()->commitTransaction($transactionId);
719                 } catch (Zend_Db_Statement_Exception $zdse) {
720                     // something went wrong
721                     // maybe another parallel request added a new synckey
722                     // we must remove data added from client
723                     if (!empty($collectionData['added'])) {
724                         foreach ($collectionData['added'] as $added) {
725                             $this->_contentStateBackend->delete($added['contentState']);
726                             $dataController->deleteEntry($collectionData['collectionId'], $added['serverId'], array());
727                         }
728                     }
729                     
730                     Syncope_Registry::getTransactionManager()->rollBack();
731                     
732                     throw $zdse;
733                 }
734                     
735                 
736                 // store current filter type
737                 try {
738                     $folderState = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
739                     $folderState->lastfiltertype = $collectionData['filterType'];
740                     $this->_folderBackend->update($folderState);
741                 } catch (Syncope_Exception_NotFound $senf) {
742                     // failed to get folderstate => should not happen but is also no problem in this state
743                     if ($this->_logger instanceof Zend_Log) 
744                         $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData['collectionId']);
745                 }
746             }
747         }
748         
749         
750         
751         return $this->_outputDom;
752     }
753 }