/** * generate MoveItems response */ public function getResponse() { $moves = $this->_outputDom->documentElement; foreach ($this->_moves as $move) { $response = $moves->appendChild($this->_outputDom->createElementNS('uri:Move', 'Response')); $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'SrcMsgId', $move['srcMsgId'])); try { $sourceFolder = $this->_folderBackend->getFolder($this->_device, $move['srcFldId']); } catch (Syncope_Exception_NotFound $senf) { $sourceFolder = null; } try { $destinationFolder = $this->_folderBackend->getFolder($this->_device, $move['dstFldId']); } catch (Syncope_Exception_NotFound $senf) { $destinationFolder = null; } if ($sourceFolder === null) { $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', Syncope_Command_MoveItems::STATUS_INVALID_SOURCE)); } else { if ($destinationFolder === null) { $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', Syncope_Command_MoveItems::STATUS_INVALID_DESTINATION)); } else { $dataController = Syncope_Data_Factory::factory($sourceFolder->class, $this->_device, $this->_syncTimeStamp); $newId = $dataController->moveItem($move['srcFldId'], $move['srcMsgId'], $move['dstFldId']); $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'Status', Syncope_Command_MoveItems::STATUS_SUCCESS)); $response->appendChild($this->_outputDom->createElementNS('uri:Move', 'DstMsgId', $newId)); } } } return $this->_outputDom; }
/** * generate ItemOperations response */ public function getResponse() { // add aditional namespaces $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase', 'uri:AirSyncBase'); $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSync', 'uri:AirSync'); $itemOperations = $this->_outputDom->documentElement; $itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncope_Command_ItemOperations::STATUS_SUCCESS)); $response = $itemOperations->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Response')); foreach ($this->_fetches as $fetch) { $fetchTag = $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Fetch')); try { $dataController = Syncope_Data_Factory::factory($fetch['store'], $this->_device, $this->_syncTimeStamp); if (isset($fetch['collectionId'])) { $properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties'); $dataController->appendXML($properties, $fetch, $fetch['serverId']); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncope_Command_ItemOperations::STATUS_SUCCESS)); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $fetch['collectionId'])); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $fetch['serverId'])); $fetchTag->appendChild($properties); } elseif (isset($fetch['fileReference'])) { $properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties'); $dataController->appendFileReference($properties, $fetch['fileReference']); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncope_Command_ItemOperations::STATUS_SUCCESS)); $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSyncBase', 'FileReference', $fetch['fileReference'])); $fetchTag->appendChild($properties); } } catch (Tinebase_Exception_NotFound $e) { $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncope_Command_ItemOperations::STATUS_ITEM_FAILED_CONVERSION)); } catch (Exception $e) { $response->appendChild($this->_outputDom->createElementNS('uri:ItemOperations', 'Status', Syncope_Command_ItemOperations::STATUS_SERVER_ERROR)); } } return $this->_outputDom; }
/** * parse FolderDelete request * */ public function handle() { $xml = simplexml_import_dom($this->_inputDom); $syncKey = (int) $xml->SyncKey; $folderId = (string) $xml->ServerId; if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is {$syncKey}"); } $this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey); try { $this->_folder = $this->_folderBackend->getFolder($this->_device, $folderId); $dataController = Syncope_Data_Factory::factory($this->_folder->class, $this->_device, $this->_syncTimeStamp); $dataController->deleteFolder($this->_folder); $this->_folderBackend->delete($this->_folder); } catch (Syncope_Exception_NotFound $senf) { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage()); } } }
/** * generate FolderCreate response */ public function getResponse() { $folderCreate = $this->_outputDom->documentElement; if ($this->_syncState == false) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " INVALID synckey"); } $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncope_Command_FolderSync::STATUS_INVALID_SYNC_KEY)); } else { $this->_syncState->counter++; $dataController = Syncope_Data_Factory::factory($this->_class, $this->_device, $this->_syncTimeStamp); $folder = $dataController->createFolder($this->_parentId, $this->_displayName, $this->_type); // create xml output $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', Syncope_Command_FolderSync::STATUS_SUCCESS)); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); $folderCreate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder['folderId'])); $folder = new Syncope_Model_Folder(array('device_id' => $this->_device, 'class' => $this->_class, 'folderid' => $folder['folderId'], 'parentid' => $folder['parentId'], 'displayname' => $folder['displayName'], 'type' => $folder['type'], 'creation_time' => $this->_syncTimeStamp, 'lastfiltertype' => null)); // store folder in state backend $this->_folderBackend->create($folder); $this->_syncStateBackend->update($this->_syncState); } return $this->_outputDom; }
/** * parse FolderUpdate request * */ public function handle() { $xml = simplexml_import_dom($this->_inputDom); $this->_syncKey = (int) $xml->SyncKey; $this->_parentId = (string) $xml->ParentId; $this->_displayName = (string) $xml->DisplayName; $this->_serverId = (string) $xml->ServerId; if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is {$this->_syncKey} parentId {$this->_parentId} name {$this->_displayName}"); } $defaultAccountId = Tinebase_Core::getPreference('Felamimail')->{Felamimail_Preference::DEFAULTACCOUNT}; $this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $this->_syncKey); try { $this->_folder = $this->_folderBackend->getFolder($this->_device, $this->_serverId); $dataController = Syncope_Data_Factory::factory($this->_folder->class, $this->_device, $this->_syncTimeStamp); $felamimail_model_folder = Felamimail_Controller_Folder::getInstance()->get($this->_folder->folderid); $dataController->updateFolder($defaultAccountId, trim($this->_displayName), $felamimail_model_folder['globalname']); $this->_folderBackend->update($this->_folder); } catch (Syncope_Exception_NotFound $senf) { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage()); } } }
/** * (non-PHPdoc) * @see Syncope_Command_Wbxml::getResponse() */ public function getResponse() { $itemEstimate = $this->_outputDom->documentElement; foreach ($this->_collections as $collectionData) { $response = $itemEstimate->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Response')); // invalid collectionid provided if (empty($collectionData['folder'])) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder does not exist"); } $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_INVALID_COLLECTION)); $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection')); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 0)); } elseif (!$collectionData['syncState'] instanceof Syncope_Model_ISyncState) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData['syncKey']} provided"); } /* * Android phones (and maybe others) don't take care about status 4(INVALID_SYNC_KEY) * To solve the problem we always return status 1(SUCCESS) even the sync key is invalid with Estimate set to 1. * This way the phone gets forced to sync. Handling invalid synckeys during sync command works without any problems. * $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_INVALID_SYNC_KEY)); $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection')); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 0)); */ $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_SUCCESS)); $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection')); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 1)); } else { $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class, $this->_device, $this->_syncTimeStamp); $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_SUCCESS)); $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection')); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class'])); $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId'])); if ($collectionData['syncState']->counter === 0) { // this is the first sync. in most cases there are data on the server. $count = count($dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType'])); } else { $count = $dataController->getCountOfChanges($this->_contentStateBackend, $collectionData['folder'], $collectionData['syncState']); } $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', $count)); } // folderState can be NULL in case of not existing folder if (isset($collectionData['folder'])) { $this->_folderBackend->update($collectionData['folder']); } } return $this->_outputDom; }
/** * 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->_inputDom instanceof DOMDocument) { $xml = simplexml_import_dom($this->_inputDom); $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; } catch (Syncope_Exception_NotFound $senf) { if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $senf->getMessage()); } $status = self::STATUS_FOLDER_NOT_FOUND; break; } } $this->_device->pingfolder = serialize($folders); } $this->_device = $this->_deviceBackend->update($this->_device); } $lifeTime = $this->_device->pinglifetime; #Tinebase_Core::setExecutionLifeTime($lifeTime); $intervalEnd = $intervalStart + $lifeTime; $secondsLeft = $intervalEnd; $folders = unserialize($this->_device->pingfolder); if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Folders to monitor({$lifeTime} / {$intervalStart} / {$intervalEnd} / {$status}): " . print_r($folders, true)); } if ($status === self::STATUS_NO_CHANGES_FOUND) { $folderWithChanges = array(); do { foreach ((array) $folders as $folder) { $dataController = Syncope_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp); try { $syncState = $this->_syncStateBackend->getSyncState($this->_device, $folder); $foundChanges = !!$dataController->getCountOfChanges($this->_contentStateBackend, $folder, $syncState); } catch (Syncope_Exception_NotFound $e) { // folder got never synchronized to client if ($this->_logger instanceof Zend_Log) { $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage()); } if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' syncstate not found. enforce sync for folder: ' . $folder->folderid); } $foundChanges = true; } if ($foundChanges == true) { $this->_foldersWithChanges[] = $folder; $status = self::STATUS_CHANGES_FOUND; } } if ($status === self::STATUS_CHANGES_FOUND) { break; } // another process synchronized data already if (isset($syncState) && $syncState->lastsync > $this->_syncTimeStamp) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " terminate ping process. Some other process updated data already."); } break; } sleep(self::PING_TIMEOUT); $secondsLeft = $intervalEnd - time(); //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " seconds left: " . $secondsLeft); } while ($secondsLeft > 0); } if ($this->_logger instanceof Zend_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->folderid)); if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " changes in folder: " . $changedFolder->folderid); } } } }
public function testDeleteEntry() { $device = $this->_deviceBackend->create(Syncope_Backend_DeviceTests::getTestDevice(Syncope_Model_Device::TYPE_IPHONE)); $dataController = Syncope_Data_Factory::factory(Syncope_Data_Factory::CLASS_CONTACTS, $device, new DateTime(null, new DateTimeZone('UTC'))); Syncope_Data_AData::$entries['Syncope_Data_Contacts']['addressbookFolderId']['foobar'] = array(); $this->assertArrayHasKey('foobar', Syncope_Data_Contacts::$entries['Syncope_Data_Contacts']['addressbookFolderId']); $dataController->deleteEntry('addressbookFolderId', 'foobar', array()); $this->assertArrayNotHasKey('foobar', Syncope_Data_Contacts::$entries['Syncope_Data_Contacts']['addressbookFolderId']); }
/** * (non-PHPdoc) * @see Syncope_Command_Wbxml::getResponse() */ public function getResponse() { $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:AirSyncBase', 'uri:AirSyncBase'); $sync = $this->_outputDom->documentElement; $collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections')); foreach ($this->_collections as $collectionData) { // invalid collectionid provided if (!$collectionData['folder'] instanceof Syncope_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 Syncope_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 = Syncope_Data_Factory::factory($collectionData['folder']->class, $this->_device, $this->_syncTimeStamp); $serverAdds = array(); $serverChanges = array(); $serverDeletes = array(); $moreAvailable = false; if ($collectionData['getChanges'] === true) { // continue sync session? if (is_array($collectionData['syncState']->pendingdata)) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state "); } $serverAdds = $collectionData['syncState']->pendingdata['serverAdds']; $serverChanges = $collectionData['syncState']->pendingdata['serverChanges']; $serverDeletes = $collectionData['syncState']->pendingdata['serverDeletes']; } else { // fetch entries added since last sync $allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData['folder']); $allServerEntries = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']); // add entries $serverDiff = array_diff($allServerEntries, $allClientEntries); // add entries which produced problems during delete from client $serverAdds = $collectionData['forceAdd']; // add entries not yet sent to client $serverAdds = array_unique(array_merge($serverAdds, $serverDiff)); # @todo still needed? foreach ($serverAdds as $id => $serverId) { // skip entries added by client during this sync session if (isset($collectionData['added'][$serverId]) && !isset($collectionData['forceAdd'][$serverId])) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId); } unset($serverAdds[$id]); } } // entries to be deleted $serverDeletes = array_diff($allClientEntries, $allServerEntries); // fetch entries changed since last sync $serverChanges = $dataController->getChangedEntries($collectionData['collectionId'], $collectionData['syncState']->lastsync, $this->_syncTimeStamp); $serverChanges = array_merge($serverChanges, $collectionData['forceChange']); foreach ($serverChanges as $id => $serverId) { // skip entry, if it got changed by client during current sync if (isset($collectionData['changed'][$serverId]) && !isset($collectionData['forceChange'][$serverId])) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId); } unset($serverChanges[$id]); } } // entries comeing in scope are already in $serverAdds and do not need to // be send with $serverCanges $serverChanges = array_diff($serverChanges, $serverAdds); } if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverAdds) . '/' . count($serverChanges) . '/' . count($serverDeletes) . ' entries for sync from server to client'); } } if (!empty($collectionData['added']) || !empty($collectionData['changed']) || !empty($collectionData['deleted']) || !empty($serverAdds) || !empty($serverChanges) || !empty($serverDeletes)) { $collectionData['syncState']->counter++; } // collection header $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)); $responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses'); // send reponse for newly added entries if (!empty($collectionData['added'])) { foreach ($collectionData['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($collectionData['changed'])) { foreach ($collectionData['changed'] as $serverId => $status) { if ($status !== Syncope_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'])) { 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->appendXML($applicationData, $collectionData, $serverId); $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS)); $fetch->appendChild($applicationData); } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage()); } $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND)); } } } if ($responses->hasChildNodes() === true) { $collection->appendChild($responses); } if (count($serverAdds) + count($serverChanges) + count($serverDeletes) > $collectionData['windowSize']) { $moreAvailable = true; $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable')); } $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands'); /** * process entries added on server side */ $newContentStates = array(); foreach ($serverAdds as $id => $serverId) { if ($this->_totalCount === $collectionData['windowSize']) { 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 Zend_Log) # $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client"); # # unset($serverAdds[$id]); # continue; # #} catch (Syncope_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->appendXML($applicationData, $collectionData, $serverId); $commands->appendChild($add); $this->_totalCount++; } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage()); } } // mark as send to the client, even the conversion to xml might have failed $newContentStates[] = new Syncope_Model_Content(array('device_id' => $this->_device, 'folder_id' => $collectionData['folder'], 'contentid' => $serverId, 'creation_time' => $this->_syncTimeStamp, 'creation_synckey' => $collectionData['syncState']->counter)); unset($serverAdds[$id]); } /** * process entries changed on server side */ foreach ($serverChanges as $id => $serverId) { if ($this->_totalCount === $collectionData['windowSize']) { 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->appendXML($applicationData, $collectionData, $serverId); $commands->appendChild($change); $this->_totalCount++; } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage()); } } unset($serverChanges[$id]); } /** * process entries deleted on server side */ $deletedContentStates = array(); foreach ($serverDeletes as $id => $serverId) { if ($this->_totalCount === $collectionData['windowSize']) { 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); $this->_totalCount++; } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) { $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage()); } } unset($serverDeletes[$id]); } if ($commands->hasChildNodes() === true) { $collection->appendChild($commands); } if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is " . $collectionData['syncState']->counter); } } if (isset($collectionData['syncState']) && $collectionData['syncState'] instanceof Syncope_Model_ISyncState && $collectionData['syncState']->counter != $collectionData['syncKey']) { // increment sync timestamp by 1 second $this->_syncTimeStamp->modify('+1 sec'); // store pending data in sync state when needed if (isset($moreAvailable) && $moreAvailable === true) { $collectionData['syncState']->pendingdata = array('serverAdds' => (array) $serverAdds, 'serverChanges' => (array) $serverChanges, 'serverDeletes' => (array) $serverDeletes); } else { $collectionData['syncState']->pendingdata = null; } if (!empty($collectionData['added'])) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries"); } $keepPreviousSyncKey = false; } else { $keepPreviousSyncKey = true; } $collectionData['syncState']->lastsync = $this->_syncTimeStamp; try { $transactionId = Syncope_Registry::getTransactionManager()->startTransaction(Syncope_Registry::getDatabase()); // store new synckey $this->_syncStateBackend->create($collectionData['syncState'], $keepPreviousSyncKey); // store contentstates for new entries added to client if (isset($newContentStates)) { foreach ($newContentStates as $state) { $this->_contentStateBackend->create($state); } } // remove contentstates for entries to be deleted on client if (isset($deletedContentStates)) { foreach ($deletedContentStates as $state) { $this->_contentStateBackend->delete($state); } } Syncope_Registry::getTransactionManager()->commitTransaction($transactionId); } catch (Zend_Db_Statement_Exception $zdse) { // something went wrong // maybe another parallel request added a new synckey // we must remove data added from client if (!empty($collectionData['added'])) { foreach ($collectionData['added'] as $added) { $this->_contentStateBackend->delete($added['contentState']); $dataController->deleteEntry($collectionData['collectionId'], $added['serverId'], array()); } } Syncope_Registry::getTransactionManager()->rollBack(); throw $zdse; } // store current filter type try { $folderState = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']); $folderState->lastfiltertype = $collectionData['filterType']; $this->_folderBackend->update($folderState); } catch (Syncope_Exception_NotFound $senf) { // failed to get folderstate => should not happen but is also no problem in this state if ($this->_logger instanceof Zend_Log) { $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData['collectionId']); } } } } return $this->_outputDom; }
/** * generate FolderSync response * * @todo changes are missing in response (folder got renamed for example) */ public function getResponse() { $folderSync = $this->_outputDom->documentElement; if ($this->_syncState === false) { if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " INVALID synckey provided"); } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY)); } else { $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS)); $adds = array(); $deletes = array(); $updates = array(); foreach ($this->_classes as $class) { try { $dataController = Syncope_Data_Factory::factory($class, $this->_device, $this->_syncTimeStamp); } catch (Zend_Exception $ze) { // backend not defined if ($this->_logger instanceof Zend_Log) { $this->_logger->info(__METHOD__ . '::' . __LINE__ . " no data backend defined for class: " . $class); } continue; } // retrieve all folders available in data backend $serverFolders = $dataController->getAllFolders(); // retrieve all folders sent to client $clientFolders = $this->_folderBackend->getFolderState($this->_device, $class); $serverFoldersIds = array_keys($serverFolders); // is this the first sync? if ($this->_syncState->counter == 0) { $clientFoldersIds = array(); } else { $clientFoldersIds = array_keys($clientFolders); } // calculate added entries $serverDiff = array_diff($serverFoldersIds, $clientFoldersIds); foreach ($serverDiff as $serverFolderId) { if (isset($clientFolders[$serverFolderId])) { $adds[] = $clientFolders[$serverFolderId]; } else { $adds[] = new Syncope_Model_Folder(array('device_id' => $this->_device, 'class' => $class, 'folderid' => $serverFolders[$serverFolderId]['folderId'], 'parentid' => $serverFolders[$serverFolderId]['parentId'], 'displayname' => $serverFolders[$serverFolderId]['displayName'], 'type' => $serverFolders[$serverFolderId]['type'], 'creation_time' => $this->_syncTimeStamp, 'lastfiltertype' => null)); } } // calculate deleted entries $serverDiff = array_diff($clientFoldersIds, $serverFoldersIds); foreach ($serverDiff as $serverFolderId) { $deletes[] = $clientFolders[$serverFolderId]; } // calculate changed entries $serverIntersect = array_intersect($clientFoldersIds, $serverFoldersIds); foreach ($serverIntersect as $serverIntersectId) { if ($serverFolders[$serverIntersectId][parentId] != $clientFolders[$serverIntersectId]->parentid or $serverFolders[$serverIntersectId][displayName] != $clientFolders[$serverIntersectId]->displayname) { $updates[] = new Syncope_Model_Folder(array('id' => $clientFolders[$serverIntersectId]->id, 'device_id' => $this->_device, 'class' => $class, 'folderid' => $serverFolders[$serverIntersectId]['folderId'], 'parentid' => $serverFolders[$serverIntersectId]['parentId'], 'displayname' => $serverFolders[$serverIntersectId]['displayName'], 'type' => $serverFolders[$serverIntersectId]['type'], 'creation_time' => $this->_syncTimeStamp, 'lastfiltertype' => null)); } } } $count = count($adds) + count($updates) + count($deletes); if ($count > 0) { $this->_syncState->counter++; } // create xml output $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $this->_syncState->counter)); $changes = $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Changes')); $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Count', $count)); foreach ($adds as $folder) { $add = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Add')); $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder->folderid)); $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ParentId', $folder->parentid)); $displayName = $this->_outputDom->createElementNS('uri:FolderHierarchy', 'DisplayName'); $displayName->appendChild($this->_outputDom->createTextNode($folder->displayname)); $add->appendChild($displayName); $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Type', $folder->type)); // store folder in backend if (empty($folder->id)) { $this->_folderBackend->create($folder); } } foreach ($updates as $folder) { $update = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Update')); $update->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder->folderid)); $update->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ParentId', $folder->parentid)); $displayName = $this->_outputDom->createElementNS('uri:FolderHierarchy', 'DisplayName'); $displayName->appendChild($this->_outputDom->createTextNode($folder->displayname)); $update->appendChild($displayName); $update->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Type', $folder->type)); $this->_folderBackend->update($folder); } foreach ($deletes as $folder) { $delete = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Delete')); $delete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folder->folderid)); $this->_folderBackend->delete($folder); } if (empty($this->_syncState->id)) { $this->_syncStateBackend->create($this->_syncState); } else { $this->_syncStateBackend->update($this->_syncState); } } return $this->_outputDom; }