Ejemplo n.º 1
0
 /**
  * process the XML file and add, change, delete or fetches data
  *
  * @todo can we get rid of LIBXML_NOWARNING
  * @todo we need to stored the initial data for folders and lifetime as the phone is sending them only when they change
  * @return resource
  */
 public function handle()
 {
     $intervalStart = time();
     $status = self::STATUS_NO_CHANGES_FOUND;
     // the client does not send a wbxml document, if the Ping parameters did not change compared with the last request
     if ($this->_requestBody instanceof DOMDocument) {
         $xml = simplexml_import_dom($this->_requestBody);
         $xml->registerXPathNamespace('Ping', 'Ping');
         if (isset($xml->HeartbeatInterval)) {
             $this->_device->pinglifetime = (int) $xml->HeartbeatInterval;
         }
         if (isset($xml->Folders->Folder)) {
             $folders = array();
             foreach ($xml->Folders->Folder as $folderXml) {
                 try {
                     // does the folder exist?
                     $folder = $this->_folderBackend->getFolder($this->_device, (string) $folderXml->Id);
                     $folders[$folder->id] = $folder;
                 } catch (Syncroton_Exception_NotFound $senf) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage());
                     }
                     $status = self::STATUS_FOLDER_NOT_FOUND;
                     break;
                 }
             }
             $this->_device->pingfolder = serialize(array_keys($folders));
         }
     }
     $this->_device->lastping = new DateTime('now', new DateTimeZone('utc'));
     if ($status == self::STATUS_NO_CHANGES_FOUND) {
         $this->_device = $this->_deviceBackend->update($this->_device);
     }
     $lifeTime = $this->_device->pinglifetime;
     $maxLifeTime = Syncroton_Registry::getMaxPingInterval();
     if ($maxLifeTime > 0 && $lifeTime > $maxLifeTime) {
         $ping = $this->_outputDom->documentElement;
         $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', self::STATUS_INTERVAL_TO_GREAT_OR_SMALL));
         $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'HeartbeatInterval', $maxLifeTime));
         return;
     }
     $intervalEnd = $intervalStart + $lifeTime;
     $secondsLeft = $intervalEnd;
     $folders = unserialize($this->_device->pingfolder);
     if ($status === self::STATUS_NO_CHANGES_FOUND && (!is_array($folders) || count($folders) == 0)) {
         $status = self::STATUS_MISSING_PARAMETERS;
     }
     if ($this->_logger instanceof Syncroton_Log) {
         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folders to monitor({$lifeTime} / {$intervalStart} / {$intervalEnd} / {$status}): " . print_r($folders, true));
     }
     if ($status === self::STATUS_NO_CHANGES_FOUND) {
         do {
             // take a break to save battery lifetime
             sleep(Syncroton_Registry::getPingTimeout());
             try {
                 $device = $this->_deviceBackend->get($this->_device->id);
             } catch (Syncroton_Exception_NotFound $e) {
                 if ($this->_logger instanceof Syncroton_Log) {
                     $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
                 }
                 $status = self::STATUS_FOLDER_NOT_FOUND;
                 break;
             } catch (Exception $e) {
                 if ($this->_logger instanceof Syncroton_Log) {
                     $this->_logger->err(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
                 }
                 // do nothing, maybe temporal issue, should we stop?
                 continue;
             }
             // if another Ping command updated lastping property, we can stop processing this Ping command request
             if (isset($device->lastping) && $device->lastping instanceof DateTime && $device->pingfolder === $this->_device->pingfolder && $device->lastping->getTimestamp() > $this->_device->lastping->getTimestamp()) {
                 break;
             }
             $now = new DateTime('now', new DateTimeZone('utc'));
             foreach ($folders as $folderId) {
                 try {
                     $folder = $this->_folderBackend->get($folderId);
                     $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
                 } catch (Syncroton_Exception_NotFound $e) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
                     }
                     $status = self::STATUS_FOLDER_NOT_FOUND;
                     break;
                 } catch (Exception $e) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->error(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
                     }
                     // do nothing, maybe temporal issue, should we stop?
                     continue;
                 }
                 try {
                     $syncState = $this->_syncStateBackend->getSyncState($this->_device, $folder);
                     // another process synchronized data of this folder already. let's skip it
                     if ($syncState->lastsync > $this->_syncTimeStamp) {
                         continue;
                     }
                     // safe battery time by skipping folders which got synchronied less than Syncroton_Registry::getQuietTime() seconds ago
                     if ($now->getTimestamp() - $syncState->lastsync->getTimestamp() < Syncroton_Registry::getQuietTime()) {
                         continue;
                     }
                     $foundChanges = $dataController->hasChanges($this->_contentStateBackend, $folder, $syncState);
                 } catch (Syncroton_Exception_NotFound $e) {
                     // folder got never synchronized to client
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
                     }
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' syncstate not found. enforce sync for folder: ' . $folder->serverId);
                     }
                     $foundChanges = true;
                 }
                 if ($foundChanges == true) {
                     $this->_foldersWithChanges[] = $folder;
                     $status = self::STATUS_CHANGES_FOUND;
                 }
             }
             if ($status != self::STATUS_NO_CHANGES_FOUND) {
                 break;
             }
             $secondsLeft = $intervalEnd - time();
             if ($this->_logger instanceof Syncroton_Log) {
                 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " seconds left: " . $secondsLeft);
             }
             // See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
             //
             // break if there are less than PingTimeout + 10 seconds left for the next loop
             // otherwise the response will be returned after the client has finished his Ping
             // request already maybe
         } while ($secondsLeft > Syncroton_Registry::getPingTimeout() + 10);
     }
     if ($this->_logger instanceof Syncroton_Log) {
         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " Lifetime: {$lifeTime} SecondsLeft: {$secondsLeft} Status: {$status})");
     }
     $ping = $this->_outputDom->documentElement;
     $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Status', $status));
     if ($status === self::STATUS_CHANGES_FOUND) {
         $folders = $ping->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Folders'));
         foreach ($this->_foldersWithChanges as $changedFolder) {
             $folder = $folders->appendChild($this->_outputDom->createElementNS('uri:Ping', 'Folder', $changedFolder->serverId));
             if ($this->_logger instanceof Syncroton_Log) {
                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " changes in folder: " . $changedFolder->serverId);
             }
         }
     }
 }
Ejemplo n.º 2
0
 /**
  * (non-PHPdoc)
  * @see Syncroton_Command_Wbxml::getResponse()
  */
 public function getResponse()
 {
     $sync = $this->_outputDom->documentElement;
     $collections = $this->_outputDom->createElementNS('uri:AirSync', 'Collections');
     $totalChanges = 0;
     // Detect devices that do not support empty Sync reponse
     $emptySyncSupported = !preg_match('/(meego|nokian800)/i', $this->_device->useragent);
     // continue only if there are changes or no time is left
     if ($this->_heartbeatInterval > 0) {
         $intervalStart = time();
         do {
             // take a break to save battery lifetime
             sleep(Syncroton_Registry::getPingTimeout());
             $now = new DateTime(null, new DateTimeZone('utc'));
             foreach ($this->_collections as $collectionData) {
                 // continue immediately if folder does not exist
                 if (!$collectionData->folder instanceof Syncroton_Model_IFolder) {
                     break 2;
                     // countinue immediately if syncstate is invalid
                 } elseif (!$collectionData->syncState instanceof Syncroton_Model_ISyncState) {
                     break 2;
                 } else {
                     if ($collectionData->getChanges !== true) {
                         continue;
                     }
                     try {
                         // just check if the folder still exists
                         $this->_folderBackend->get($collectionData->folder);
                     } catch (Syncroton_Exception_NotFound $senf) {
                         if ($this->_logger instanceof Syncroton_Log) {
                             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " collection does not exist anymore: " . $collectionData->collectionId);
                         }
                         $collectionData->getChanges = false;
                         // make sure this is the last while loop
                         // no break 2 here, as we like to check the other folders too
                         $intervalStart -= $this->_heartbeatInterval;
                     }
                     // check that the syncstate still exists and is still valid
                     try {
                         $syncState = $this->_syncStateBackend->getSyncState($this->_device, $collectionData->folder);
                         // another process synchronized data of this folder already. let's skip it
                         if ($syncState->id !== $collectionData->syncState->id) {
                             if ($this->_logger instanceof Syncroton_Log) {
                                 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " syncstate changed during heartbeat interval for collection: " . $collectionData->folder->serverId);
                             }
                             $collectionData->getChanges = false;
                             // make sure this is the last while loop
                             // no break 2 here, as we like to check the other folders too
                             $intervalStart -= $this->_heartbeatInterval;
                         }
                     } catch (Syncroton_Exception_NotFound $senf) {
                         if ($this->_logger instanceof Syncroton_Log) {
                             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " no syncstate found anymore for collection: " . $collectionData->folder->serverId);
                         }
                         $collectionData->syncState = null;
                         // make sure this is the last while loop
                         // no break 2 here, as we like to check the other folders too
                         $intervalStart -= $this->_heartbeatInterval;
                     }
                     // safe battery time by skipping folders which got synchronied less than Syncroton_Command_Ping::$quietTime seconds ago
                     if (!$collectionData->syncState instanceof Syncroton_Model_SyncState || $now->getTimestamp() - $collectionData->syncState->lastsync->getTimestamp() < Syncroton_Registry::getQuietTime()) {
                         continue;
                     }
                     $dataController = Syncroton_Data_Factory::factory($collectionData->folder->class, $this->_device, $this->_syncTimeStamp);
                     // countinue immediately if there are any changes available
                     if ($dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState)) {
                         break 2;
                     }
                 }
             }
             // See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
             //
             // break if there are less than PingTimeout + 10 seconds left for the next loop
             // otherwise the response will be returned after the client has finished his Ping
             // request already maybe
         } while (time() - $intervalStart < $this->_heartbeatInterval - (Syncroton_Registry::getPingTimeout() + 10));
     }
     foreach ($this->_collections as $collectionData) {
         $collectionChanges = 0;
         /**
          * keep track of entries added on server side
          */
         $newContentStates = array();
         /**
          * keep track of entries deleted on server side
          */
         $deletedContentStates = array();
         // invalid collectionid provided
         if (!$collectionData->folder instanceof Syncroton_Model_IFolder) {
             $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
             // invalid synckey provided
         } elseif (!$collectionData->syncState instanceof Syncroton_Model_ISyncState) {
             // set synckey to 0
             $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
             // initial sync
         } elseif ($collectionData->syncState->counter === 0) {
             $collectionData->syncState->counter++;
             // initial sync
             // send back a new SyncKey only
             $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
             if (!empty($collectionData->folder->class)) {
                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
             }
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData->syncState->counter));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
         } else {
             $dataController = Syncroton_Data_Factory::factory($collectionData->folder->class, $this->_device, $this->_syncTimeStamp);
             $clientModifications = $this->_modifications[$collectionData->collectionId];
             $serverModifications = array('added' => array(), 'changed' => array(), 'deleted' => array());
             if ($collectionData->getChanges === true) {
                 // continue sync session?
                 if (is_array($collectionData->syncState->pendingdata)) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
                     }
                     $serverModifications = $collectionData->syncState->pendingdata;
                 } elseif ($dataController->hasChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState)) {
                     // update _syncTimeStamp as $dataController->hasChanges might have spent some time
                     $this->_syncTimeStamp = new DateTime(null, new DateTimeZone('utc'));
                     try {
                         // fetch entries added since last sync
                         $allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData->folder);
                         $allServerEntries = $dataController->getServerEntries($collectionData->collectionId, $collectionData->options['filterType']);
                         // add entries
                         $serverDiff = array_diff($allServerEntries, $allClientEntries);
                         // add entries which produced problems during delete from client
                         $serverModifications['added'] = $clientModifications['forceAdd'];
                         // add entries not yet sent to client
                         $serverModifications['added'] = array_unique(array_merge($serverModifications['added'], $serverDiff));
                         // @todo still needed?
                         foreach ($serverModifications['added'] as $id => $serverId) {
                             // skip entries added by client during this sync session
                             if (isset($clientModifications['added'][$serverId]) && !isset($clientModifications['forceAdd'][$serverId])) {
                                 if ($this->_logger instanceof Syncroton_Log) {
                                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
                                 }
                                 unset($serverModifications['added'][$id]);
                             }
                         }
                         // entries to be deleted
                         $serverModifications['deleted'] = array_diff($allClientEntries, $allServerEntries);
                         // fetch entries changed since last sync
                         $serverModifications['changed'] = $dataController->getChangedEntries($collectionData->collectionId, $collectionData->syncState->lastsync, $this->_syncTimeStamp, $collectionData->options['filterType']);
                         $serverModifications['changed'] = array_merge($serverModifications['changed'], $clientModifications['forceChange']);
                         foreach ($serverModifications['changed'] as $id => $serverId) {
                             // skip entry, if it got changed by client during current sync
                             if (isset($clientModifications['changed'][$serverId]) && !isset($clientModifications['forceChange'][$serverId])) {
                                 if ($this->_logger instanceof Syncroton_Log) {
                                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
                                 }
                                 unset($serverModifications['changed'][$id]);
                             } else {
                                 if (isset($clientModifications['added'][$serverId]) && !isset($clientModifications['forceAdd'][$serverId])) {
                                     if ($this->_logger instanceof Syncroton_Log) {
                                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped change for added entry: " . $serverId);
                                     }
                                     unset($serverModifications['changed'][$id]);
                                 }
                             }
                         }
                         // entries comeing in scope are already in $serverModifications['added'] and do not need to
                         // be send with $serverCanges
                         $serverModifications['changed'] = array_diff($serverModifications['changed'], $serverModifications['added']);
                     } catch (Exception $e) {
                         if ($this->_logger instanceof Syncroton_Log) {
                             $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getMessage());
                         }
                         if ($this->_logger instanceof Syncroton_Log) {
                             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folder state checking failed: " . $e->getTraceAsString());
                         }
                         // Prevent from removing client entries when getServerEntries() fails
                         // @todo: should we set Status and break the loop here?
                         $serverModifications = array('added' => array(), 'changed' => array(), 'deleted' => array());
                     }
                 }
                 if ($this->_logger instanceof Syncroton_Log) {
                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverModifications['added']) . '/' . count($serverModifications['changed']) . '/' . count($serverModifications['deleted']) . ' entries for sync from server to client');
                 }
             }
             // collection header
             $collection = $this->_outputDom->createElementNS('uri:AirSync', 'Collection');
             if (!empty($collectionData->folder->class)) {
                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
             }
             $syncKeyElement = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey'));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
             $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
             $responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
             // send reponse for newly added entries
             if (!empty($clientModifications['added'])) {
                 foreach ($clientModifications['added'] as $entryData) {
                     $add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
                     $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
                     // we have no serverId is the add failed
                     if (isset($entryData['serverId'])) {
                         $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
                     }
                     $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
                 }
             }
             // send reponse for changed entries
             if (!empty($clientModifications['changed'])) {
                 foreach ($clientModifications['changed'] as $serverId => $status) {
                     if ($status !== Syncroton_Command_Sync::STATUS_SUCCESS) {
                         $change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
                         $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                         $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
                     }
                 }
             }
             // send response for to be fetched entries
             if (!empty($collectionData->toBeFetched)) {
                 // unset all truncation settings as entries are not allowed to be truncated during fetch
                 $fetchCollectionData = clone $collectionData;
                 // unset truncationSize
                 if (isset($fetchCollectionData->options['bodyPreferences']) && is_array($fetchCollectionData->options['bodyPreferences'])) {
                     foreach ($fetchCollectionData->options['bodyPreferences'] as $key => $bodyPreference) {
                         unset($fetchCollectionData->options['bodyPreferences'][$key]['truncationSize']);
                     }
                 }
                 $fetchCollectionData->options['mimeTruncation'] = Syncroton_Command_Sync::TRUNCATE_NOTHING;
                 foreach ($collectionData->toBeFetched as $serverId) {
                     $fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
                     $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                     try {
                         $applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
                         $dataController->getEntry($fetchCollectionData, $serverId)->appendXML($applicationData, $this->_device);
                         $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
                         $fetch->appendChild($applicationData);
                     } catch (Exception $e) {
                         if ($this->_logger instanceof Syncroton_Log) {
                             $this->_logger->warning(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
                         }
                         if ($this->_logger instanceof Syncroton_Log) {
                             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getTraceAsString());
                         }
                         $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
                     }
                 }
             }
             if ($responses->hasChildNodes() === true) {
                 $collection->appendChild($responses);
             }
             $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
             foreach ($serverModifications['added'] as $id => $serverId) {
                 if ($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
                     break;
                 }
                 #/**
                 # * somewhere is a problem in the logic for handling moreAvailable
                 # *
                 # * it can happen, that we have a contentstate (which means we sent the entry to the client
                 # * and that this entry is yet in $collectionData->syncState->pendingdata['serverAdds']
                 # * I have no idea how this can happen, but the next lines of code work around this problem
                 # */
                 try {
                     $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry({$serverId}) which is already on the client");
                     }
                     unset($serverModifications['added'][$id]);
                     continue;
                 } catch (Syncroton_Exception_NotFound $senf) {
                     // do nothing => content state should not exist yet
                 }
                 try {
                     $add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
                     $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                     $applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
                     $dataController->getEntry($collectionData, $serverId)->appendXML($applicationData, $this->_device);
                     $commands->appendChild($add);
                     $collectionChanges++;
                 } catch (Syncroton_Exception_MemoryExhausted $seme) {
                     // continue to next entry, as there is not enough memory left for the current entry
                     // this will lead to MoreAvailable at the end and the entry will be synced during the next Sync command
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->warning(__METHOD__ . '::' . __LINE__ . " memory exhausted for entry: " . $serverId);
                     }
                     continue;
                 } catch (Exception $e) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->warning(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
                     }
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getTraceAsString());
                     }
                 }
                 // mark as sent to the client, even the conversion to xml might have failed
                 $newContentStates[] = new Syncroton_Model_Content(array('device_id' => $this->_device, 'folder_id' => $collectionData->folder, 'contentid' => $serverId, 'creation_time' => $this->_syncTimeStamp, 'creation_synckey' => $collectionData->syncState->counter + 1));
                 unset($serverModifications['added'][$id]);
             }
             /**
              * process entries changed on server side
              */
             foreach ($serverModifications['changed'] as $id => $serverId) {
                 if ($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
                     break;
                 }
                 try {
                     $change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
                     $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                     $applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
                     $dataController->getEntry($collectionData, $serverId)->appendXML($applicationData, $this->_device);
                     $commands->appendChild($change);
                     $collectionChanges++;
                 } catch (Syncroton_Exception_MemoryExhausted $seme) {
                     // continue to next entry, as there is not enough memory left for the current entry
                     // this will lead to MoreAvailable at the end and the entry will be synced during the next Sync command
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->warning(__METHOD__ . '::' . __LINE__ . " memory exhausted for entry: " . $serverId);
                     }
                     continue;
                 } catch (Exception $e) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->warning(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
                     }
                 }
                 unset($serverModifications['changed'][$id]);
             }
             foreach ($serverModifications['deleted'] as $id => $serverId) {
                 if ($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
                     break;
                 }
                 try {
                     // check if we have sent this entry to the phone
                     $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
                     $delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
                     $delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                     $deletedContentStates[] = $state;
                     $commands->appendChild($delete);
                     $collectionChanges++;
                 } catch (Exception $e) {
                     if ($this->_logger instanceof Syncroton_Log) {
                         $this->_logger->warning(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
                     }
                 }
                 unset($serverModifications['deleted'][$id]);
             }
             $countOfPendingChanges = count($serverModifications['added']) + count($serverModifications['changed']) + count($serverModifications['deleted']);
             if ($countOfPendingChanges > 0) {
                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
             } else {
                 $serverModifications = null;
             }
             if ($commands->hasChildNodes() === true) {
                 $collection->appendChild($commands);
             }
             $totalChanges += $collectionChanges;
             // increase SyncKey if needed
             if (!empty($clientModifications['added']) || !empty($clientModifications['changed']) || !empty($clientModifications['deleted']) || $commands->hasChildNodes() === true || $collectionData->syncState->pendingdata != $serverModifications) {
                 // ...then increase SyncKey
                 $collectionData->syncState->counter++;
             }
             $syncKeyElement->appendChild($this->_outputDom->createTextNode($collectionData->syncState->counter));
             if ($this->_logger instanceof Syncroton_Log) {
                 $this->_logger->info(__METHOD__ . '::' . __LINE__ . " current synckey is " . $collectionData->syncState->counter);
             }
             if (!$emptySyncSupported || $collection->childNodes->length > 4 || $collectionData->syncState->counter != $collectionData->syncKey) {
                 $collections->appendChild($collection);
             }
         }
         if (isset($collectionData->syncState) && $collectionData->syncState instanceof Syncroton_Model_ISyncState && $collectionData->syncState->counter != $collectionData->syncKey) {
             if ($this->_logger instanceof Syncroton_Log) {
                 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " update syncState for collection: " . $collectionData->collectionId);
             }
             // store pending data in sync state when needed
             if (isset($countOfPendingChanges) && $countOfPendingChanges > 0) {
                 $collectionData->syncState->pendingdata = array('added' => (array) $serverModifications['added'], 'changed' => (array) $serverModifications['changed'], 'deleted' => (array) $serverModifications['deleted']);
             } else {
                 $collectionData->syncState->pendingdata = null;
             }
             if (!empty($clientModifications['added'])) {
                 if ($this->_logger instanceof Syncroton_Log) {
                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
                 }
                 $keepPreviousSyncKey = false;
             } else {
                 $keepPreviousSyncKey = true;
             }
             $collectionData->syncState->lastsync = clone $this->_syncTimeStamp;
             // increment sync timestamp by 1 second
             $collectionData->syncState->lastsync->modify('+1 sec');
             try {
                 $transactionId = Syncroton_Registry::getTransactionManager()->startTransaction(Syncroton_Registry::getDatabase());
                 // store new synckey
                 $this->_syncStateBackend->create($collectionData->syncState, $keepPreviousSyncKey);
                 // store contentstates for new entries added to client
                 foreach ($newContentStates as $state) {
                     $this->_contentStateBackend->create($state);
                 }
                 // remove contentstates for entries to be deleted on client
                 foreach ($deletedContentStates as $state) {
                     $this->_contentStateBackend->delete($state);
                 }
                 Syncroton_Registry::getTransactionManager()->commitTransaction($transactionId);
                 //} catch (Zend_Db_Statement_Exception $zdse) {
             } catch (Exception $zdse) {
                 // something went wrong
                 // maybe another parallel request added a new synckey
                 // we must remove data added from client
                 if (!empty($clientModifications['added'])) {
                     foreach ($clientModifications['added'] as $added) {
                         $this->_contentStateBackend->delete($added['contentState']);
                         $dataController->deleteEntry($collectionData->collectionId, $added['serverId'], array());
                     }
                 }
                 Syncroton_Registry::getTransactionManager()->rollBack();
                 throw $zdse;
             }
         }
         // store current filter type
         try {
             $folderState = $this->_folderBackend->get($collectionData->folder);
             $folderState->lastfiltertype = $collectionData->options['filterType'];
             if ($folderState->isDirty()) {
                 $this->_folderBackend->update($folderState);
             }
         } catch (Syncroton_Exception_NotFound $senf) {
             // failed to get folderstate => should not happen but is also no problem in this state
             if ($this->_logger instanceof Syncroton_Log) {
                 $this->_logger->warning(__METHOD__ . '::' . __LINE__ . ' failed to get folder state for: ' . $collectionData->collectionId);
             }
         }
     }
     if ($collections->hasChildNodes() === true) {
         $sync->appendChild($collections);
     }
     if ($sync->hasChildNodes()) {
         return $this->_outputDom;
     }
     return null;
 }