/** * generate FolderUpdate response * * @todo currently we support only the main folder which contains all contacts/tasks/events/notes per class */ public function getResponse($_keepSession = FALSE) { $folderUpdate = $this->_outputDom->documentElement; if ($this->_syncKey > '0' && $this->_controller->validateSyncKey($this->_device, $this->_syncKey, 'FolderSync') === false) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " INVALID synckey"); $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', ActiveSync_Command_FolderSync::STATUS_INVALID_SYNC_KEY)); } else { $newSyncKey = $this->_syncKey + 1; // create xml output $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', ActiveSync_Command_FolderSync::STATUS_SUCCESS)); $folderUpdate->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $newSyncKey)); $this->_controller->updateSyncKey($this->_device, $newSyncKey, $this->_syncTimeStamp, 'FolderSync'); } return $this->_outputDom; }
/** * generate FolderSync response * * @todo currently we support only the main folder which contains all contacts/tasks/events/notes per class * * @param boolean $_keepSession keep session active(don't logout user) when true */ public function getResponse($_keepSession = FALSE) { $folderSync = $this->_outputDom->documentElement; if ($this->_syncKey > '0' && $this->_controller->validateSyncKey($this->_device, $this->_syncKey, 'FolderSync') === false) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " INVALID synckey provided"); $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY)); } else { $adds = array(); $deletes = array(); $count = 0; if ($this->_syncKey == 0) { $this->_folderStateBackend->resetState($this->_device); } foreach ($this->_classes as $class) { $dataController = ActiveSync_Controller::dataFactory($class, $this->_device, $this->_syncTimeStamp); try { $folders = $dataController->getSupportedFolders(); } catch (Exception $e) { if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) { Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . " failed to get folders for class {$class}. " . $e->getMessage()); } if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) { Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . " failed to get folders for class {$class}. " . $e->getTraceAsString()); } continue; } if ($this->_syncKey == 0) { foreach ($folders as $folderId => $folder) { $adds[$class][$folderId] = $folder; $count++; } } else { $allServerEntries = array_keys($folders); $allClientEntries = $this->_folderStateBackend->getClientState($this->_device, $class); // added entries $serverDiff = array_diff($allServerEntries, $allClientEntries); foreach ($serverDiff as $folderId) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " add {$class} {$folderId}"); } $adds[$class][$folderId] = $folders[$folderId]; $count++; } // deleted entries $serverDiff = array_diff($allClientEntries, $allServerEntries); foreach ($serverDiff as $folderId) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " delete {$class} {$folderId}"); } $deletes[$class][$folderId] = $folderId; $count++; } } } if ($count > 0) { $newSyncKey = $this->_syncKey + 1; } else { $newSyncKey = $this->_syncKey; } // create xml output $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS)); $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'SyncKey', $newSyncKey)); $changes = $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Changes')); $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Count', $count)); foreach ($adds as $class => $folders) { foreach ((array) $folders 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'])); $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'DisplayName', $folder['displayName'])); $add->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Type', $folder['type'])); $this->_addFolderState($class, $folder['folderId']); if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " {$class} => " . $folder['folderId']); } } } foreach ($deletes as $class => $folders) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " {$class}"); } foreach ((array) $folders as $folderId) { $delete = $changes->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Delete')); $delete->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'ServerId', $folderId)); $this->_deleteFolderState($class, $folderId); } } $this->_controller->updateSyncKey($this->_device, $newSyncKey, $this->_syncTimeStamp, 'FolderSync'); } return $this->_outputDom; }
/** * process the XML file and add, change, delete or fetches data */ public function handle() { // input xml $xml = new SimpleXMLElement($this->_inputDom->saveXML()); #$xml = simplexml_import_dom($this->_inputDom); foreach ($xml->Collections->Collection as $xmlCollection) { $collectionData = array('syncKey' => (int) $xmlCollection->SyncKey, 'syncKeyValid' => true, 'class' => isset($xmlCollection->Class) ? (string) $xmlCollection->Class : null, 'collectionId' => (string) $xmlCollection->CollectionId, 'windowSize' => isset($xmlCollection->WindowSize) ? (int) $xmlCollection->WindowSize : 100, 'deletesAsMoves' => isset($xmlCollection->DeletesAsMoves) ? true : false, 'getChanges' => isset($xmlCollection->GetChanges) ? true : false, 'added' => array(), 'changed' => array(), 'deleted' => array(), 'forceAdd' => array(), 'forceChange' => array(), 'toBeFetched' => array(), 'filterType' => 0, 'mimeSupport' => self::MIMESUPPORT_DONT_SEND_MIME, 'mimeTruncation' => ActiveSync_Command_Sync::TRUNCATE_NOTHING, 'bodyPreferences' => array()); // process options if (isset($xmlCollection->Options)) { // optional parameters if (isset($xmlCollection->Options->FilterType)) { $collectionData['filterType'] = (int) $xmlCollection->Options->FilterType; } if (isset($xmlCollection->Options->MIMESupport)) { $collectionData['mimeSupport'] = (int) $xmlCollection->Options->MIMESupport; } if (isset($xmlCollection->Options->MIMETruncation)) { $collectionData['mimeTruncation'] = (int) $xmlCollection->Options->MIMETruncation; } // try to fetch element from AirSyncBase:BodyPreference $airSyncBase = $xmlCollection->Options->children('uri:AirSyncBase'); if (isset($airSyncBase->BodyPreference)) { foreach ($airSyncBase->BodyPreference as $bodyPreference) { $type = (int) $bodyPreference->Type; $collectionData['bodyPreferences'][$type] = array('type' => $type); // optional if (isset($bodyPreference->TruncationSize)) { $collectionData['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize; } } } } // does the folder exist for this device try { $folder = $this->getFolderState($this->_device, $collectionData['collectionId']); // newer clients don't send the class tag anymore $collectionData['class'] = $folder->class; } catch (Tinebase_Exception_NotFound $e) { if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) { Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData['collectionId']} not found"); } $collectionData['syncState'] = new ActiveSync_Model_SyncState(array('device_id' => $this->_device->getId(), 'counter' => 0, 'type' => $collectionData['collectionId'], 'lastsync' => $this->_syncTimeStamp)); $this->_collections['collectionNotFound'][$collectionData['collectionId']] = $collectionData; continue; } if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData['syncKey']} Class: {$collectionData['class']} CollectionId: {$collectionData['collectionId']}"); } // initial synckey if ($collectionData['syncKey'] === 0) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided"); } $collectionData['syncState'] = new ActiveSync_Model_SyncState(array('device_id' => $this->_device->getId(), 'counter' => $collectionData['syncKey'], 'type' => $collectionData['class'] . '-' . $collectionData['collectionId'], 'lastsync' => $this->_syncTimeStamp)); $this->_collections[$collectionData['class']][$collectionData['collectionId']] = $collectionData; continue; } // check for invalid sycnkey if (($collectionData['syncState'] = $this->_controller->validateSyncKey($this->_device, $collectionData['syncKey'], $collectionData['class'], $collectionData['collectionId'])) === false) { if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) { Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData['syncKey']} provided"); } $collectionData['syncKeyValid'] = false; $collectionData['syncState'] = new ActiveSync_Model_SyncState(array('device_id' => $this->_device->getId(), 'counter' => $collectionData['syncKey'], 'type' => $collectionData['class'] . '-' . $collectionData['collectionId'], 'lastsync' => $this->_syncTimeStamp)); $this->_collections[$collectionData['class']][$collectionData['collectionId']] = $collectionData; continue; } $dataController = ActiveSync_Controller::dataFactory($collectionData['class'], $this->_device, $this->_syncTimeStamp); // handle incoming data if (isset($xmlCollection->Commands->Add)) { $adds = $xmlCollection->Commands->Add; if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server"); } foreach ($adds as $add) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId); } // search for existing entries if first sync // @todo: maybe this can be removed // good phones don't send entries at synckey 1 // and if they sent, maybe they also send entries at synckey 2 too if ($collectionData['syncKey'] == 1) { $existing = $dataController->search($collectionData['collectionId'], $add->ApplicationData); } else { $existing = array(); // count() == 0 } try { if (count($existing) === 0) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " entry not found. adding as new"); } $added = $dataController->add($collectionData['collectionId'], $add->ApplicationData); } else { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found matching entry. reuse existing entry"); } // use the first found entry $added = $existing[0]; } $collectionData['added'][(string) $add->ClientId]['serverId'] = $added->getId(); $collectionData['added'][(string) $add->ClientId]['status'] = self::STATUS_SUCCESS; $this->_addContentState($collectionData['class'], $collectionData['collectionId'], $added->getId()); } catch (Exception $e) { if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) { Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage()); } $collectionData['added'][(string) $add->ClientId]['status'] = self::STATUS_SERVER_ERROR; } } } // handle changes, but only if not first sync if ($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Change)) { $changes = $xmlCollection->Commands->Change; if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server"); } foreach ($changes as $change) { $serverId = (string) $change->ServerId; try { $changed = $dataController->change($collectionData['collectionId'], $serverId, $change->ApplicationData); $collectionData['changed'][$serverId] = self::STATUS_SUCCESS; } catch (Tinebase_Exception_AccessDenied $e) { $collectionData['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT; $collectionData['forceChange'][$serverId] = $serverId; } catch (Tinebase_Exception_NotFound $e) { // entry does not exist anymore, will get deleted automaticaly $collectionData['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND; } catch (Exception $e) { if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) { Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e); } // something went wrong while trying to update the entry $collectionData['changed'][$serverId] = self::STATUS_SERVER_ERROR; } } } // handle deletes, but only if not first sync if (isset($xmlCollection->Commands->Delete)) { $deletes = $xmlCollection->Commands->Delete; if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server"); } foreach ($deletes as $delete) { $serverId = (string) $delete->ServerId; try { // check if we have send this entry to the phone $this->_controller->getContentState($this->_device, $collectionData['class'], $collectionData['collectionId'], $serverId); try { $dataController->delete($collectionData['collectionId'], $serverId, $collectionData); } catch (Tinebase_Exception_NotFound $e) { if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) { Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found'); } } catch (Tinebase_Exception $e) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage()); } $collectionData['forceAdd'][$serverId] = $serverId; } } catch (Tinebase_Exception_NotFound $tenf) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already'); } // should we send a special status??? //$collectionData['deleted'][$serverId] = self::STATUS_SUCCESS; } $collectionData['deleted'][$serverId] = self::STATUS_SUCCESS; $this->_deleteContentState($collectionData['class'], $collectionData['collectionId'], $serverId); } } // handle fetches, but only if not first sync if ($collectionData['syncKey'] > 1 && isset($xmlCollection->Commands->Fetch)) { // the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0 // unfortunately the iPhone dont set GetChanges to 0 when fetching email body, but is confused when we send // changes if (!isset($xmlCollection->GetChanges)) { $collectionData['getChanges'] = false; } $fetches = $xmlCollection->Commands->Fetch; if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server"); } foreach ($fetches as $fetch) { $serverId = (string) $fetch->ServerId; $collectionData['toBeFetched'][$serverId] = $serverId; } } $this->_collections[$collectionData['class']][$collectionData['collectionId']] = $collectionData; } }