diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php index 9c563bf..ae0cce9 100644 --- a/lib/ext/Syncroton/Command/FolderSync.php +++ b/lib/ext/Syncroton/Command/FolderSync.php @@ -1,267 +1,288 @@ */ /** * class to handle ActiveSync FolderSync command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml { const STATUS_SUCCESS = 1; const STATUS_FOLDER_EXISTS = 2; const STATUS_IS_SPECIAL_FOLDER = 3; const STATUS_FOLDER_NOT_FOUND = 4; const STATUS_PARENT_FOLDER_NOT_FOUND = 5; const STATUS_SERVER_ERROR = 6; const STATUS_ACCESS_DENIED = 7; const STATUS_REQUEST_TIMED_OUT = 8; const STATUS_INVALID_SYNC_KEY = 9; const STATUS_MISFORMATTED = 10; const STATUS_UNKNOWN_ERROR = 11; /** * some usefull constants for working with the xml files */ const FOLDERTYPE_GENERIC_USER_CREATED = 1; const FOLDERTYPE_INBOX = 2; const FOLDERTYPE_DRAFTS = 3; const FOLDERTYPE_DELETEDITEMS = 4; const FOLDERTYPE_SENTMAIL = 5; const FOLDERTYPE_OUTBOX = 6; const FOLDERTYPE_TASK = 7; const FOLDERTYPE_CALENDAR = 8; const FOLDERTYPE_CONTACT = 9; const FOLDERTYPE_NOTE = 10; const FOLDERTYPE_JOURNAL = 11; const FOLDERTYPE_MAIL_USER_CREATED = 12; const FOLDERTYPE_CALENDAR_USER_CREATED = 13; const FOLDERTYPE_CONTACT_USER_CREATED = 14; const FOLDERTYPE_TASK_USER_CREATED = 15; const FOLDERTYPE_JOURNAL_USER_CREATED = 16; const FOLDERTYPE_NOTE_USER_CREATED = 17; const FOLDERTYPE_UNKOWN = 18; protected $_defaultNameSpace = 'uri:FolderHierarchy'; protected $_documentElement = 'FolderSync'; protected $_classes = array( Syncroton_Data_Factory::CLASS_CALENDAR, Syncroton_Data_Factory::CLASS_CONTACTS, Syncroton_Data_Factory::CLASS_EMAIL, Syncroton_Data_Factory::CLASS_NOTES, Syncroton_Data_Factory::CLASS_TASKS ); /** * @var string */ protected $_syncKey; /** * parse FolderSync request * */ public function handle() { $xml = simplexml_import_dom($this->_requestBody); $syncKey = (int)$xml->SyncKey; if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " synckey is $syncKey"); if ($syncKey === 0) { $this->_syncState = new Syncroton_Model_SyncState(array( 'device_id' => $this->_device, 'counter' => 0, 'type' => 'FolderSync', 'lastsync' => $this->_syncTimeStamp )); // reset state of foldersync $this->_syncStateBackend->resetState($this->_device, 'FolderSync'); return; } if (!($this->_syncState = $this->_syncStateBackend->validate($this->_device, 'FolderSync', $syncKey)) instanceof Syncroton_Model_SyncState) { $this->_syncStateBackend->resetState($this->_device, 'FolderSync'); } } /** * generate FolderSync response * * @todo changes are missing in response (folder got renamed for example) */ public function getResponse() { $folderSync = $this->_outputDom->documentElement; // invalid synckey provided if (!$this->_syncState instanceof Syncroton_Model_SyncState) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " invalid synckey provided. FolderSync 0 needed."); $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_INVALID_SYNC_KEY)); return $this->_outputDom; } // send headers from options command also when FolderSync SyncKey is 0 if ($this->_syncState->counter == 0) { $optionsCommand = new Syncroton_Command_Options(); $this->_headers = array_merge($this->_headers, $optionsCommand->getHeaders()); } $adds = array(); $updates = array(); $deletes = array(); foreach($this->_classes as $class) { try { $dataController = Syncroton_Data_Factory::factory($class, $this->_device, $this->_syncTimeStamp); } catch (Exception $e) { // backend not defined if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " no data backend defined for class: " . $class); continue; } try { // retrieve all folders available in data backend $serverFolders = $dataController->getAllFolders(); // retrieve all folders sent to client $clientFolders = $this->_folderBackend->getFolderState($this->_device, $class); if ($this->_syncState->counter > 0) { // retrieve all folders changed since last sync $changedFolders = $dataController->getChangedFolders($this->_syncState->lastsync, $this->_syncTimeStamp); } else { $changedFolders = array(); } // only folders which were sent to the client already are allowed to be in $changedFolders $changedFolders = array_intersect_key($changedFolders, $clientFolders); } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getMessage()); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " Syncing folder hierarchy failed: " . $e->getTraceAsString()); // The Status element is global for all collections. If one collection fails, // a failure status MUST be returned for all collections. if ($e instanceof Syncroton_Exception_Status) { $status = $e->getCode(); } else { $status = Syncroton_Exception_Status_FolderSync::UNKNOWN_ERROR; } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', $status)); return $this->_outputDom; } $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) { // have we created a folderObject in syncroton_folder before? if (isset($clientFolders[$serverFolderId])) { $add = $clientFolders[$serverFolderId]; } else { $add = $serverFolders[$serverFolderId]; $add->creationTime = $this->_syncTimeStamp; $add->deviceId = $this->_device; unset($add->id); } $add->class = $class; $adds[] = $add; } // calculate changed entries foreach ($changedFolders as $changedFolder) { $change = $clientFolders[$changedFolder->serverId]; $change->displayName = $changedFolder->displayName; $change->parentId = $changedFolder->parentId; $change->type = $changedFolder->type; $updates[] = $change; } - + + // Find changes in case backend does not support folder changes detection. + // On some backends getChangedFolders() can return an empty result. + // We make sure all is up-to-date comparing folder properties. + foreach ($clientFoldersIds as $folderId) { + if (isset($serverFolders[$folderId])) { + $c = $clientFolders[$folderId]; + $s = $serverFolders[$folderId]; + + if ($c->displayName !== $s->displayName + || strval($c->parentId) !== strval($s->parentId) + || $c->type != $s->type + ) { + $c->displayName = $s->displayName; + $c->parentId = $s->parentId; + $c->type = $s->type; + + $updates[] = $c; + } + } + } + // calculate deleted entries $serverDiff = array_diff($clientFoldersIds, $serverFoldersIds); foreach ($serverDiff as $serverFolderId) { $deletes[] = $clientFolders[$serverFolderId]; } } $folderSync->appendChild($this->_outputDom->createElementNS('uri:FolderHierarchy', 'Status', self::STATUS_SUCCESS)); $count = count($adds) + count($updates) + count($deletes); if($count > 0) { $this->_syncState->counter++; $this->_syncState->lastsync = $this->_syncTimeStamp; } // 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')); $folder->appendXML($add, $this->_device); // 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')); $folder->appendXML($update, $this->_device); $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->serverId)); $this->_folderBackend->delete($folder); } if (empty($this->_syncState->id)) { $this->_syncStateBackend->create($this->_syncState); } else { $this->_syncStateBackend->update($this->_syncState); } return $this->_outputDom; } }