/** * (non-PHPdoc) * @see Syncope/Syncope_TestCase::setUp() */ protected function setUp() { Syncope_Registry::setDatabase(getTestDatabase()); Syncope_Registry::setTransactionManager(Syncope_TransactionManager::getInstance()); Syncope_Registry::getTransactionManager()->startTransaction(Syncope_Registry::getDatabase()); #$writer = new Zend_Log_Writer_Null(); $writer = new Zend_Log_Writer_Stream('php://output'); $writer->addFilter(new Zend_Log_Filter_Priority($this->_logPriority)); $logger = new Zend_Log($writer); $this->_deviceBackend = new Syncope_Backend_Device(Syncope_Registry::getDatabase()); $this->_folderBackend = new Syncope_Backend_Folder(Syncope_Registry::getDatabase()); $this->_syncStateBackend = new Syncope_Backend_SyncState(Syncope_Registry::getDatabase()); $this->_contentStateBackend = new Syncope_Backend_Content(Syncope_Registry::getDatabase()); $this->_device = $this->_deviceBackend->create(Syncope_Backend_DeviceTests::getTestDevice()); Syncope_Registry::set('deviceBackend', $this->_deviceBackend); Syncope_Registry::set('folderStateBackend', $this->_folderBackend); Syncope_Registry::set('syncStateBackend', $this->_syncStateBackend); Syncope_Registry::set('contentStateBackend', $this->_contentStateBackend); Syncope_Registry::set('loggerBackend', $logger); Syncope_Registry::setContactsDataClass('Syncope_Data_Contacts'); Syncope_Registry::setCalendarDataClass('Syncope_Data_Calendar'); Syncope_Registry::setEmailDataClass('Syncope_Data_Email'); Syncope_Registry::setTasksDataClass('Syncope_Data_Tasks'); }
/** * (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; }