From: Michael J. Rubinsky Date: Fri, 23 Apr 2010 21:56:03 +0000 (-0400) Subject: Implement a working history state driver. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=2a65aa27f3a76a931a2ceae9629f77eb2e905ab0;p=horde.git Implement a working history state driver. --- diff --git a/framework/ActiveSync/lib/Horde/ActiveSync.php b/framework/ActiveSync/lib/Horde/ActiveSync.php index 821d5f105..66ce6e252 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync.php @@ -301,6 +301,10 @@ class Horde_ActiveSync const FOLDER_TYPE_RECIPIENT_CACHE = 19; const FOLDER_TYPE_DUMMY = '__dummy.Folder.Id__'; + const CHANGE_ORIGIN_PIM = 0; + const CHANGE_ORIGIN_SERVER = 1; + const CHANGE_ORIGIN_NA = 3; + /** * Logger * diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php index 1fbc84927..65cc34b1e 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Connector/Importer.php @@ -117,7 +117,7 @@ class Horde_ActiveSync_Connector_Importer $change['mod'] = 0; $change['parent'] = $this->_folderId; $change['flags'] = (isset($message->read)) ? $message->read : 0; - $this->_state->updateState('change', $change); + $this->_state->updateState('change', $change, Horde_ActiveSync::CHANGE_ORIGIN_NA); /* If this is a conflict, see if the server wins */ if ($conflict && $this->_flags == Horde_ActiveSync::CONFLICT_OVERWRITE_PIM) { @@ -127,12 +127,13 @@ class Horde_ActiveSync_Connector_Importer /* Tell the backend about the change */ $stat = $this->_backend->changeMessage($this->_folderId, $id, $message); + $stat['parent'] = $this->_folderId; if (!is_array($stat)) { return $stat; } /* Record the state of the message */ - $this->_state->updateState('change', $stat); + $this->_state->updateState('change', $stat, Horde_ActiveSync::CHANGE_ORIGIN_PIM); return $stat['id']; } @@ -158,7 +159,8 @@ class Horde_ActiveSync_Connector_Importer /* Update client state */ $change = array(); $change['id'] = $id; - $this->_state->updateState('delete', $change); + $change['mod'] = time(); + $this->_state->updateState('delete', $change, Horde_ActiveSync::CHANGE_ORIGIN_PIM, $this->_folderId); /* If server wins the conflict, don't import change - it will be * detected on next sync and sent back to PIM (since we updated the PIM @@ -190,7 +192,7 @@ class Horde_ActiveSync_Connector_Importer $change = array(); $change['id'] = $id; $change['flags'] = $flags; - $this->_state->updateState('flags', $change); + $this->_state->updateState('flags', $change, Horde_ActiveSync::CHANGE_ORIGIN_NA); /* Tell backend */ $this->_backend->SetReadFlag($this->_folderId, $id, $flags); @@ -236,13 +238,13 @@ class Horde_ActiveSync_Connector_Importer $change['mod'] = $displayname; $change['parent'] = $parent; $change['flags'] = 0; - $this->_state->updateState('change', $change); + $this->_state->updateState('change', $change, Horde_ActiveSync::CHANGE_ORIGIN_NA); } /* Tell the backend */ $stat = $this->_backend->ChangeFolder($parent, $id, $displayname, $type); if ($stat) { - $this->_state->updateState('change', $stat); + $this->_state->updateState('change', $stat, Horde_ActiveSync::CHANGE_ORIGIN_NA); } return $stat['id']; @@ -266,7 +268,7 @@ class Horde_ActiveSync_Connector_Importer $change = array(); $change['id'] = $id; - $this->_state->updateState('delete', $change); + $this->_state->updateState('delete', $change, Horde_ActiveSync::CHANGE_ORIGIN_NA); $this->_backend->DeleteFolder($parent, $id); return true; diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php index 7c2d45137..292e2c2ff 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php @@ -222,7 +222,7 @@ abstract class Horde_ActiveSync_Driver_Base * @return array A list of messge uids that have chnaged in the specified * time period. */ - abstract public function getServerChanges($folderId, $from_ts, $to_ts); + abstract public function getServerChanges($folderId, $from_ts, $to_ts, $cutoffdate); /** * Get a message stat. diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php index bf6dc5fe6..cf1081f6a 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php @@ -257,14 +257,78 @@ class Horde_ActiveSync_Driver_Horde extends Horde_ActiveSync_Driver_Base * @return array A list of messge uids that have chnaged in the specified * time period. */ - public function getServerChanges($folderId, $from_ts, $to_ts) + public function getServerChanges($folderId, $from_ts, $to_ts, $cutoffdate) { - $adds = $this->_connector->calendar_listBy('add', $from_ts); - $changes = $this->_connector->calendar_listBy('modify', $from_ts); - $deletes = $this->_connector->calendar_listBy('delete', $from_ts); - // FIXME: Need to filter the results by $from_ts OR need to fix // Horde_History to query for a timerange instead of a single timestamp + $this->_logger->debug("Horde_ActiveSync_Driver_Horde::getServerChanges($folderId, $from_ts, $to_ts, $cutoffdate)"); + switch ($folderId) { + case self::APPOINTMENTS_FOLDER: + if ($from_ts == 0) { + /* Can't use History if it's a first sync */ + $startstamp = (int)$cutoffdate; + $endstamp = time() + 32140800; //60 * 60 * 24 * 31 * 12 == one year + $events = $this->_connector->calendar_listEvents($startstamp, $endstamp); + foreach ($events as $day) { + foreach($day as $e) { + $adds[] = $e->uid; + } + } + $edits = $deletes = array(); + } else { + $adds = $this->_connector->calendar_listBy('add', $from_ts); + $edits = $this->_connector->calendar_listBy('modify', $from_ts); + $deletes = $this->_connector->calendar_listBy('delete', $from_ts); + } + break; + case self::CONTACTS_FOLDER: + /* Can't use History for first sync */ + if ($from_ts == 0) { + $adds = $this->_connector->contacts_list(); + $edits = $deletes = array(); + } else { + $adds = $this->_connector->contacts_listBy('add', $from_ts); + $edits = $this->_connector->contacts_listBy('modify', $from_ts); + $deletes = $this->_connector->contacts_listBy('delete', $from_ts); + } + break; + case self::TASKS_FOLDER: + /* Can't use History for first sync */ + if ($from_ts == 0) { + $adds = $this->_connector->tasks_listTasks(); + $edits = $deletes = array(); + } else { + $adds = $this->_connector->tasks_listBy('add', $from_ts); + $edits = $this->_connector->tasks_listBy('modify', $from_ts); + $deletes = $this->_connector->tasks_listBy('delete', $from_ts); + } + break; + } + + /* Build the changes array */ + $changes = array(); + /* Server additions */ + foreach ($adds as $add) { + $changes[] = array( + 'id' => $add, + 'type' => 'change', + 'flags' => Horde_ActiveSync::FLAG_NEWMESSAGE); + } + + /* Server changes */ + foreach ($edits as $change) { + $changes[] = array( + 'id' => $change, + 'type' => 'change'); + + } + + /* Server Deletions */ + foreach ($deletes as $deleted) { + $changes[] = array( + 'id' => $deleted, + 'type' => 'deletion'); + } return $changes; } @@ -389,7 +453,11 @@ class Horde_ActiveSync_Driver_Horde extends Horde_ActiveSync_Driver_Base $this->_logger->err($e->getMessage()); return false; } + /* There is no history entry for new messages, so use the + * current time for purposes of remembering this is from the PIM + */ $stat = $this->_smartStatMessage($folderid, $id, false); + $stat['mod'] = time(); } else { // ActiveSync messages do NOT contain the serverUID value, put // it in ourselves so we can have it during import/change. diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php index c5a4051e5..64c0dda10 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php @@ -55,7 +55,6 @@ class Horde_ActiveSync_Driver_Horde_Connector_Registry /** * Get a list of event uids that have had $action happen since $from_ts. - * Optionally limits to a specific calendar. * * @param string $action The action to check for (add, modify, delete) * @param timestamp $from_ts The timestamp to start checking from @@ -64,7 +63,7 @@ class Horde_ActiveSync_Driver_Horde_Connector_Registry */ public function calendar_listBy($action, $from_ts) { - return $this->_registry->calendar->listBy($action, $from_ts); + return $this->_registry->calendar->listBy($action, (int)$from_ts); } /** @@ -206,6 +205,19 @@ class Horde_ActiveSync_Driver_Horde_Connector_Registry } /** + * Get a list of contact uids that have had $action happen since $from_ts. + * + * @param string $action The action to check for (add, modify, delete) + * @param timestamp $from_ts The timestamp to start checking from + * + * @return array An array of event uids + */ + public function contacts_listBy($action, $from_ts) + { + return $this->_registry->contacts->listBy($action, (int)$from_ts); + } + + /** * List all tasks in the user's default tasklist. * * @return array An array of task uids. @@ -217,6 +229,11 @@ class Horde_ActiveSync_Driver_Horde_Connector_Registry return $this->_registry->tasks->listTaskUids($tasklist); } + public function tasks_listTaskLists() + { + return $this->_registry->tasks->listTaskLists(); + } + /** * Export a single task from the backend. * @@ -280,6 +297,19 @@ class Horde_ActiveSync_Driver_Horde_Connector_Registry } /** + * Get a list of task uids that have had $action happen since $from_ts. + * + * @param string $action The action to check for (add, modify, delete) + * @param timestamp $from_ts The timestamp to start checking from + * + * @return array An array of event uids + */ + public function tasks_listBy($action, $from_ts) + { + return $this->_registry->tasks->listBy($action, (int)$from_ts); + } + + /** * Return all active api interfaces. * * @return array An array of interface names. diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php index 043c0e13f..6d911d367 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php @@ -71,10 +71,11 @@ class Horde_ActiveSync_Request_FolderSync extends Horde_ActiveSync_Request_Base /* Initialize state engine */ $state = &$this->_driver->getStateObject(array('synckey' => $synckey)); + $state->getDeviceInfo($devId); try { /* Get folders that we know about already */ - $state->loadState($synckey); - + $state->loadState($synckey, 'foldersync'); + /* Get new synckey to send back */ $newsynckey = $state->getNewSyncKey($synckey); } catch (Horde_ActiveSync_Exception $e) { diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/GetItemEstimate.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/GetItemEstimate.php index aa6ed02c2..871fa01e0 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/GetItemEstimate.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/GetItemEstimate.php @@ -116,6 +116,7 @@ class Horde_ActiveSync_Request_GetItemEstimate extends Horde_ActiveSync_Request_ /* compatibility mode - get id from state */ if (!isset($collectionid)) { $state = &$this->_driver->getStateObject(); + $state->getDeviceInfo($devId); $collectionid = $state>getFolderData($this->_devid, $collection['class']); } $collection['id'] = $collectionid; diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php index 46efd88ab..df0a5c752 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php @@ -76,7 +76,7 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base $timeout = $this->_ping_settings['waitinterval']; /* Notify */ - $this->_logger->info('[' . $devId . '] Ping received at timestamp: ' . $now . '.'); + $this->_logger->info('[' . $devId . '] PING received at timestamp: ' . $now . '.'); /* Glass half full kinda guy... */ $this->_statusCode = self::STATUS_NOCHANGES; @@ -160,7 +160,12 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base $collection = $collections[$i]; $collection['synckey'] = $this->_devId; $sync = $this->_driver->getSyncObject(); - $this->_state->loadPingCollectionState($collection); + try { + $this->_state->loadPingCollectionState($collection); + } catch (Horde_ActiveSync_Exception $e) { + $this->_logger->err('PING terminating: ' . $e->getMessage()); + break; + } try { $sync->init($this->_state, null, $collection); } catch (Horde_ActiveSync_Exception $e) { diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php index cd1e0d0e8..23be4d56f 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php @@ -75,6 +75,7 @@ class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base /* Get state object */ $state = $this->_driver->getStateObject(); + $state->getDeviceInfo($devId); /* Handle android remote wipe */ if ($this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_REMOTEWIPE)) { diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php index 2a05afbd8..85ac80772 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php @@ -186,8 +186,9 @@ class Horde_ActiveSync_Request_Sync extends Horde_ActiveSync_Request_Base if ($this->_statusCode == self::STATUS_SUCCESS) { /* Initialize the state */ $state = &$this->_driver->getStateObject($collection); + $state->getDeviceInfo($devId); try { - $state->loadState($collection['synckey']); + $state->loadState($collection['synckey'], 'sync'); } catch (Horde_ActiveSync_Exception $e) { $this->_statusCode = self::STATUS_KEYMISM; $this->_handleError($collection); diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php index 0f9d3cba5..38106efc3 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php @@ -133,11 +133,12 @@ abstract class Horde_ActiveSync_State_Base * Loads the initial state from storage for the specified syncKey and * intializes the stateMachine for use. * - * @param string $key The key for the syncState or pingState to load. + * @param string $syncKey The key for the state to load. + * @param string $type Treat the loaded state data as this type of state. * * @return array The state array */ - abstract public function loadState($syncKey); + abstract public function loadState($syncKey, $type = null, $id = ''); /** * Load/initialize the ping state for the specified device. @@ -176,7 +177,7 @@ abstract class Horde_ActiveSync_State_Base * @param $change * @param $key */ - abstract public function updateState($type, $change); + abstract public function updateState($type, $change, $origin = Horde_ActiveSync::CHANGE_ORIGIN_NA); /** * Obtain the diff between PIM and server @@ -399,5 +400,100 @@ abstract class Horde_ActiveSync_State_Base return 0; // unlimited } } + /** + * Helper function that performs the actual diff between PIM state and + * server state arrays. + * + * @param array $old The PIM state + * @param array $new The current server state + * + * @return unknown_type + */ + protected function _getDiff($old, $new) + { + $changes = array(); + + // Sort both arrays in the same way by ID + usort($old, array(__CLASS__, 'RowCmp')); + usort($new, array(__CLASS__, 'RowCmp')); + + $inew = 0; + $iold = 0; + + // Get changes by comparing our list of messages with + // our previous state + while (1) { + $change = array(); + + if ($iold >= count($old) || $inew >= count($new)) { + break; + } + + if ($old[$iold]['id'] == $new[$inew]['id']) { + // Both messages are still available, compare flags and mod + if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) { + // Flags changed + $change['type'] = 'flags'; + $change['id'] = $new[$inew]['id']; + $change['flags'] = $new[$inew]['flags']; + $changes[] = $change; + } + + if ($old[$iold]['mod'] != $new[$inew]['mod']) { + $change['type'] = 'change'; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + } + + $inew++; + $iold++; + } else { + if ($old[$iold]['id'] > $new[$inew]['id']) { + // Message in state seems to have disappeared (delete) + $change['type'] = 'delete'; + $change['id'] = $old[$iold]['id']; + $changes[] = $change; + $iold++; + } else { + // Message in new seems to be new (add) + $change['type'] = 'change'; + $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + $inew++; + } + } + } + + while ($iold < count($old)) { + // All data left in _syncstate have been deleted + $change['type'] = 'delete'; + $change['id'] = $old[$iold]['id']; + $changes[] = $change; + $iold++; + } + + while ($inew < count($new)) { + // All data left in new have been added + $change['type'] = 'change'; + $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE; + $change['id'] = $new[$inew]['id']; + $changes[] = $change; + $inew++; + } + + return $changes; + } + /** + * Helper function for the _diff method + * + * @param $a + * @param $b + * @return unknown_type + */ + static public function RowCmp($a, $b) + { + return $a['id'] < $b['id'] ? 1 : -1; + } } \ No newline at end of file diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php index 5ca258b12..805105a44 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php @@ -100,11 +100,12 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base * Load the sync state * * @param string $syncKey The synckey + * @prarm string $type Treat loaded state as this type of state. * * @return void * @throws Horde_ActiveSync_Exception */ - public function loadState($syncKey) + public function loadState($syncKey, $type = null, $id = '') { /* Ensure state directory is present */ $this->_ensureUserDirectory(); @@ -189,7 +190,7 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base * * @return void */ - public function updateState($type, $change) + public function updateState($type, $change, $origin = Horde_ActiveSync::CHANGE_ORIGIN_NA) { if (empty($this->_stateCache)) { $this->_stateCache = array(); @@ -200,7 +201,7 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base /* If we are a change and don't already have a mod time, stat the * message. This would only happen when exporting a server side * change. We need the mod time to track the version of the message - * on the PIM. + * on the PIM. (Folder changes will already have a mod value) */ if (!isset($change['mod'])) { $change = $this->_backend->statMessage($this->_collection['id'], $change['id']); @@ -705,101 +706,4 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base $this->_haveStateDirectory = true; } - /** - * Helper function that performs the actual diff between PIM state and - * server state arrays. - * - * @param array $old The PIM state - * @param array $new The current server state - * - * @return unknown_type - */ - private function _getDiff($old, $new) - { - $changes = array(); - - // Sort both arrays in the same way by ID - usort($old, array(__CLASS__, 'RowCmp')); - usort($new, array(__CLASS__, 'RowCmp')); - - $inew = 0; - $iold = 0; - - // Get changes by comparing our list of messages with - // our previous state - while (1) { - $change = array(); - - if ($iold >= count($old) || $inew >= count($new)) { - break; - } - - if ($old[$iold]['id'] == $new[$inew]['id']) { - // Both messages are still available, compare flags and mod - if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) { - // Flags changed - $change['type'] = 'flags'; - $change['id'] = $new[$inew]['id']; - $change['flags'] = $new[$inew]['flags']; - $changes[] = $change; - } - - if ($old[$iold]['mod'] != $new[$inew]['mod']) { - $change['type'] = 'change'; - $change['id'] = $new[$inew]['id']; - $changes[] = $change; - } - - $inew++; - $iold++; - } else { - if ($old[$iold]['id'] > $new[$inew]['id']) { - // Message in state seems to have disappeared (delete) - $change['type'] = 'delete'; - $change['id'] = $old[$iold]['id']; - $changes[] = $change; - $iold++; - } else { - // Message in new seems to be new (add) - $change['type'] = 'change'; - $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE; - $change['id'] = $new[$inew]['id']; - $changes[] = $change; - $inew++; - } - } - } - - while ($iold < count($old)) { - // All data left in _syncstate have been deleted - $change['type'] = 'delete'; - $change['id'] = $old[$iold]['id']; - $changes[] = $change; - $iold++; - } - - while ($inew < count($new)) { - // All data left in new have been added - $change['type'] = 'change'; - $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE; - $change['id'] = $new[$inew]['id']; - $changes[] = $change; - $inew++; - } - - return $changes; - } - - /** - * Helper function for the _diff method - * - * @param $a - * @param $b - * @return unknown_type - */ - static public function RowCmp($a, $b) - { - return $a['id'] < $b['id'] ? 1 : -1; - } - } diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php index e5e4d096c..0b91aef2f 100644 --- a/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php +++ b/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php @@ -1,6 +1,33 @@ + * syncStateTable (horde_activesync_state): + * sync_time: timestamp of last sync + * sync_key: the syncKey for the last sync + * sync_data: If the last sync resulted in a MOREAVAILABLE, this contains + * a list of UIDs that still need to be sent to the PIM. If + * this sync_key represents a FOLDERSYNC state, then this + * contains the current folder state on the PIM. + * sync_devid: The device id. + * sync_folderid: The folder id for this sync. + * + * syncMapTable (horde_activesync_map): + * message_uid - The server uid for the object + * sync_modtime - The time the change was received from the PIM and + * applied to the server data store. + * sync_key - The syncKey that was current at the time the change + * was received. + * sync_devid - The device id this change was done on. + * + * syncDeviceTable (horde_activesync_device: + * device_id - The unique id for this device + * device_type - The device type the PIM identifies itself with + * device_agent - The user agent string sent by the device + * device_ping - The device's current PING state information. + * device_policykey - The current policykey for this device + * deivce_rwstatus - The current remote wipe status for this device + * * * Copyright 2010 The Horde Project (http://www.horde.org) * @@ -11,7 +38,7 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base { /** * Cache for ping state - * + * @TODO: look at moving this to base class * @var array */ private $_pingState; @@ -21,15 +48,14 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base * * @var timestamp */ - private $_lastSyncTS; + private $_lastSyncTS = 0; /** * The current sync timestamp * * @var timestamp */ - private $_thisSyncTS; - + private $_thisSyncTS = 0; /** * Local cache of changes that need to be sent @@ -39,6 +65,20 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base private $_changes; /** + * Local cache of state only used for FOLDERSYNC requests. + * + * @var array + */ + private $_state; + + /** + * The type of request we are handling (if important). + * + * @var string + */ + private $_type; + + /** * DB handle * * @var Horde_Db_Adapter_Base @@ -46,13 +86,25 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base protected $_db; /** + * The current syncKey + * + * @var string + */ + protected $_syncKey; + + /* TODO - config these */ + protected $_syncStateTable = 'horde_activesync_state'; + protected $_syncMapTable = 'horde_activesync_map'; + protected $_syncDeviceTable = 'horde_activesync_device'; + + /** * Const'r * * @param array $params Must contain: * 'db' - Horde_Db - * 'syncStateTable' - Name of table for storing syncstate - * 'pingTable' - Name of table for storing ping data - * 'syncChangesTable' - Name of table for remembering what changes + * 'syncStateTable' - Name of table for storing syncstate + * 'syncDeviceTable' - Name of table for storing device and ping data + * 'syncMapTable' - Name of table for remembering what changes * are due to PIM import so we don't mirror the * changes back to the PIM on next Sync * @@ -61,41 +113,71 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base public function __construct($params = array()) { parent::__construct($params); - if (empty($this->_params['db']) || !($this->_params['db'] instanceof Horde_Db_Adapter_Base)) { throw new InvalidArgumentException('Missing or invalid Horde_Db parameter.'); } - $this->_params = $params['db']; + + $this->_params = $params; + $this->_db = $params['db']; } /** * Load the sync state * + * @param string $syncKey The synckey of the state to load. If empty will + * force a reset of the state for the class + * specified in $id + * @prarm string $type The type of state (sync, foldersync). + * @param string $id The folder id this state represents. If empty + * assumed to be a foldersync state. + * * @return void * @throws Horde_ActiveSync_Exception */ - public function loadState($syncKey, $username) + public function loadState($syncKey, $type = null, $id = '') { if (empty($syncKey)) { + $this->_state = array(); + $this->_resetDeviceState($id); return; } + $this->_type = $type; - // Check if synckey is allowed + $this->_logger->debug(sprintf('[%s] Loading state for synckey %s', $this->_devId, $syncKey)); + /* Check if synckey is allowed */ if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) { throw new Horde_ActiveSync_Exception('Invalid sync key'); } $this->_syncKey = $syncKey; + /* Cleanup all older syncstates */ + $this->_gc($syncKey); + + /* Load the previous syncState from storage */ try { - $results = $this->_db->selectOne('SELECT sync_data, sync_devId, sync_time FROM ' . $this->_syncStateTable . ' WHERE sync_key = ?', array($this->_syncKey)); + $results = $this->_db->selectOne('SELECT sync_data, sync_devid, sync_time FROM ' + . $this->_syncStateTable . ' WHERE sync_key = ?', array($this->_syncKey)); } catch (Horde_Db_Exception $e) { throw new Horde_ActiveSync_Exception($e); } + if (!$results) { + throw new Horde_ActiveSync_Exception('Sync State Not Found.'); + } - /* Load the previous syncState from storage */ - $this->_lastSyncTS = $results['sync_time']; - $this->_devId = $results['sync_devId']; - $this->_changes = unserialize(sync_data); + /* Load the last known sync time for this collection */ + $this->_lastSyncTS = !empty($results['sync_time']) ? $results['sync_time'] : 0; + + /* Restore any state or pending changes */ + if ($type == 'foldersync') { + $state = unserialize($results['sync_data']); + $this->_state = ($state !== false) ? $state : array(); + } elseif ($type == 'sync') { + $changes = unserialize($results['sync_data']); + $this->_changes = ($changes !== false) ? $changes : null; + if ($this->_changes) { + $this->_logger->debug(sprintf('[%s] Found %n changes remaining from previous SYNC.'), $this->_devId, count($this->_changes)); + } + } } /** @@ -130,55 +212,72 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base */ public function save() { - // Update state table to remember this last synctime and key - $sql = 'INSERT INTO ' . $this->_syncStateTable . ' (sync_key, sync_data, sync_devId, sync_time) VALUES (?, ?, ?, ?)'; + $this->_logger->debug(sprintf('[%s] Saving state for synckey %s', $this->_devId, $this->_syncKey)); + + /* Update state table to remember this last synctime and key */ + $sql = 'INSERT INTO ' . $this->_syncStateTable + . ' (sync_key, sync_data, sync_devid, sync_time, sync_folderid) VALUES (?, ?, ?, ?, ?)'; /* Remember any left over changes */ - $data = (isset($this->_changes) ? serialize($this->_changes) : serialize(array())); + if ($this->_type == 'foldersync') { + $data = (isset($this->_state) ? serialize($this->_state) : ''); + } elseif ($this->_type == 'sync') { + $data = (isset($this->_changes) ? serialize(array_values($this->_changes)) : ''); + } else { + $data = ''; + } try { - $this->_db->insert($sql, array($this->_syncKey, $data, $this->_devId, $this->_thisSyncTS)); + $this->_db->insert($sql, array($this->_syncKey, $data, $this->_devId, $this->_thisSyncTS, !empty($this->_collection['id']) ? $this->_collection['id'] : false)); } catch (Horde_Db_Exception $e) { - throw new Horde_ActiveSync_Exception($e); + /* Might exist already if the last sync attempt failed. */ + $this->_db->delete('DELETE FROM ' . $this->_syncStateTable . ' WHERE sync_key = ?', array($this->_syncKey)); + $this->_db->insert($sql, array($this->_syncKey, $data, $this->_devId, $this->_thisSyncTS, $this->_collection['id'])); } - + return true; } /** * Update the state to reflect changes * - * Notes: Since PIM changes are dealt with before Server changes, we can - * use a null $_changes array to detect what we are updating for. If we - * are importing PIM changes, need to update the syncChangesTable so we - * don't mirror back the changes on next sync. If we are exporting server - * changes, we need to track which changes have been sent (by removing them - * from _changes) so we know which items to send on the next sync if a - * MOREAVAILBLE response was needed. + * Notes: If we are importing PIM changes, need to update the syncMapTable + * so we don't mirror back the changes on next sync. If we are exporting + * server changes, we need to track which changes have been sent (by + * removing them from $this->_changes) so we know which items to send on the + * next sync if a MOREAVAILBLE response was needed. * - * @param string $type The type of change (change, delete, flags) - * @param array $change Array describing change + * @param string $type The type of change (change, delete, flags) + * @param array $change A stat/change hash describing the change + * @param integer $origin Flag to indicate the origin of the change. * * @return void */ - public function updateState($type, $change) + public function updateState($type, $change, $origin = Horde_ActiveSync::CHANGE_ORIGIN_NA) { - if (!isset($this->_changes)) { - /* We must be updating state during receiving changes from PIM */ - $sql = 'INSERT INTO ' . $this->_syncChangesTable . ' (message_uid, sync_mod_time, sync_key) VALUES (?, ?, ?)'; - try { - $this->_db->insert($sql, array($change['id'], time(), $this->_syncKey)); - } catch (Horde_Db_Exception $e) { + if ($origin == Horde_ActiveSync::CHANGE_ORIGIN_PIM) { + /* We must be updating state during receiving changes from PIM */ + //$sql = 'DELETE FROM ' . $this->_syncMapTable . ' WHERE message_uid = ? AND sync_devid = ?'; + //try { + // $this->_db->delete($sql, array($change['id'], $this->_devId)); + //} catch (Horde_Db_Exception $e) { + // throw new Horde_ActiveSync_Exception($e); + //} + $sql = 'INSERT INTO ' . $this->_syncMapTable . ' (message_uid, sync_modtime, sync_key, sync_devid, sync_folderid) VALUES (?, ?, ?, ?, ?)'; + try { + $this->_db->insert($sql, array($change['id'], $change['mod'], $this->_syncKey, $this->_devId, $change['parent'])); + } catch (Horde_Db_Exception $e) { throw new Horde_ActiveSync_Exception($e); - } - } else { + } + } else { /* When sending server changes, $this->_changes will contain all * changes. Need to track which ones are sent since we might not * send all of them. */ - for ($i = 0; $i < count($this->_changes); $i++) { - if ($this->_changes[$i]['id'] == $change['id']) { - unset($this->_changes[$i]); + foreach ($this->_changes as $key => $value) { + if ($value['id'] == $change['id']) { + unset($this->_changes[$key]); + break; } } } @@ -192,6 +291,7 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base * * @return boolean * @throws Horde_ActiveSync_Exception + * @TODO */ public function setFolderData($devId, $folders) { @@ -235,6 +335,7 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base * @param string $class The folder class to fetch (Calendar, Contacts etc.) * * @return mixed Either an array of folder data || false + * @TODO */ public function getFolderData($devId, $class) { @@ -252,62 +353,151 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base // return false; } - public function getKnownFolders($syncKey) + /** + * Return an array of known folders. This is essentially the state for a + * FOLDERSYNC request. AS uses a seperate synckey for FOLDERSYNC requests + * also, so need to treat it as any other collection. + * + * @return array + */ + public function getKnownFolders() { + //@TODO: Look at moving this to the base class + /* folder state would have been loaded already in laodState() */ + if (!isset($this->_state)) { + throw new Horde_ActiveSync_Exception('Sync state not loaded'); + } + $folders = array(); + foreach ($this->_state as $folder) { + $folders[] = $folder['id']; + } - $sql = 'SELECT state_data from ' . $this->_table . ' WHERE state_syncKey = ?'; - // - // + return $folders; + } + /** + * Perform any initialization needed to deal with pingStates + * For this driver + * + * @param string $devId The device id of the PIM to load PING state for + * + * @return The $collection array + */ + public function initPingState($devId) + { + /* This would normally already be loaded by getDeviceInfo() but we + * should verify we have the correct device loaded etc... */ + if (!isset($this->_pingState) || $this->_devId !== $devId) { + $this->getDeviceInfo($devId); + } + + /* Need to get the last sync time for this collection */ + return $this->_pingState['collections']; } - public function setKnownFolders($syncKey, $folders) + /** + * Obtain the device object. + * + * @param string $devId + * + * @return StdClass + */ + public function getDeviceInfo($devId) { - $sql = 'INSERT INTO ' . $this->_table . '....'; + $this->_devId = $devId; - // Need to GC the table, delete all but the *two* most recent synckeys - // for this devId. Need the latest one, but also the previous one in - // case the device did not correctly receive the response - it will - // continue to send the previous syncKey, so we need to remember the - // state. + $query = 'SELECT device_type, device_agent, device_ping, device_policykey, device_rwstatus FROM ' + . $this->_syncDeviceTable . ' WHERE device_id = ?'; + try { + $result = $this->_db->selectOne($query, array($devId)); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + $device = new StdClass(); + if ($result) { + $device->policykey = $result['device_policykey']; + $device->rwstatus = $result['device_rwstatus']; + $device->deviceType = $result['device_type']; + $device->userAgent = $result['device_agent']; + $device->id = $devId; + if ($result['device_ping']) { + $this->_pingState = unserialize($result['device_ping']); + } else { + $this->resetPingState(); + } + } else { + /* Default structure */ + $device->policykey = 0; + $device->rwstatus = 0; // ?? + $device->deviceType = ''; + $device->userAgent = ''; + $device->id = $devId; + } + return $device; } /** - * Perform any initialization needed to deal with pingStates - * For this driver, it loads the device's state file. + * @TODO: move to base class? + */ + public function resetPingState() + { + $this->_pingState = array( + 'lifetime' => 0, + 'collections' => array()); + } + + /** + * Set new device info * - * @param string $devId The device id of the PIM to load PING state for + * @TODO: for this driver, we can add private methods to set/update some + * of these fields instead of rewriting the whole record. * - * @return The $collection array + * @param string $devId The device id. + * @param StdClass $data The device information + * + * @return boolean */ - public function initPingState($devId) + public function setDeviceInfo($devId, $data) { - $this->_devId = $devId; + /* Delete the old entry, just in case */ + $query = 'DELETE FROM ' . $this->_syncDeviceTable . ' WHERE device_id = ?'; + $this->_db->execute($query, array($devId)); - $sql = 'SELECT ping_state FROM ' . $this->_pingTable . ' WHERE ping_devid = ?'; + $query = 'INSERT INTO ' . $this->_syncDeviceTable + . '(device_type, device_agent, device_ping, device_policykey, device_rwstatus, device_id)' + . ' VALUES(?, ?, ?, ?, ?, ?)'; - $this->_pingState = unserialize($results); - // Try to get pingstate from SQL (need lifetime and last synctime) - //$this->_pingState = unserialize($sqlResults); + $values = array($data->deviceType, $data->userAgent, '', $data->policykey, $data->rwstatus, $devId); + $this->_devId = $devId; + $this->_db->insert($query, $values); + } - // If no existing state - initialize - // $this->_pingState = array( - // 'lifetime' => 0, - // 'collections' => array()); + /** + * Check that a given device id is known to the server. This is regardless + * of Provisioning status. + * + * @param string $devId + * + * @return boolean + */ + public function deviceExists($devId) + { + $query = 'SELECT COUNT(*) FROM ' . $this->_syncDeviceTable . ' WHERE device_id = ?'; - return $this->_pingState['collections']; + return $this->_db->selectValue($query, array($devId)); } /** - * Load a specific collection's ping state + * Load a specific collection's ping state. Ping state must already have + * been loaded. * * @param array $pingCollection The collection array from the PIM request * * @return void * @throws Horde_ActiveSync_Exception */ - public function loadCollectionPingState($pingCollection) + public function loadPingCollectionState($pingCollection) { if (empty($this->_pingState)) { throw new Horde_ActiveSync_Exception('PING state not initialized'); @@ -318,51 +508,37 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base /* Load any existing state */ // @TODO: I'm almost positive we need to key these by 'id', not 'class' // but this is what z-push did so... + $this->_logger->debug('Attempting to load PING state for: ' . $pingCollection['class']); if (!empty($this->_pingState['collections'][$pingCollection['class']])) { $this->_collection = $this->_pingState['collections'][$pingCollection['class']]; $this->_collection['synckey'] = $this->_devId; - //$this->_stateCache = $this->_collection['state']; + /* Set the lastSyncTS to the last time PING knows about */ + $this->_lastSyncTS = $this->_getLastSyncTS(); + $this->_logger->debug('Obtained lasst sync time for ' . $pingCollection['class'] . ' - ' . $this->_lastSyncTS); + if ($this->_lastSyncTS === false) { + // State has disappeared, perhaps a forced re-synch. + throw new Horde_ActiveSync_Exception('Previous syncstate has been removed.'); + } + + /* See if we explicitly ask to kill the PING */ + $haveState = true; } /* Initialize state for this collection */ if (!$haveState) { - $this->_logger->debug('Empty state for '. $pingCollection['class']); - - /* Start with empty state cache */ - //$this->_stateCache[$pingCollection['id']] = array(); + $this->_logger->info('[' . $this->_devId . '] Empty state for '. $pingCollection['class']); /* Init members for the getChanges call */ - $this->_syncKey = $this->_devId; $this->_collection = $pingCollection; $this->_collection['synckey'] = $this->_devId; - $this->_collection['state'] = array(); - + $this->_lastSyncTS = $this->_getLastSyncTS(); + if ($this->_lastSyncTS === false) { + // No previous SYNC issued, or it has disappeared. + throw new Horde_ActiveSync_Exception('No previous SYNC command?'); + } /* If we are here, then the pingstate was empty, prime it */ $this->_pingState['collections'][$this->_collection['class']] = $this->_collection; - - /* Need to load _stateCache so getChanges has it */ - $this->_stateCache = array(); - - $changes = $this->getChanges(); - foreach ($changes as $change) { - switch ($change['type']) { - case 'change': - $stat = $this->_backend->statMessage($this->_collection['id'], $change['id']); - if (!$message = $this->_backend->getMessage($this->_collection['id'], $change['id'], 0)) { - throw new Horde_ActiveSync_Exception('Message not found'); - } - if ($stat && $message) { - $this->updateState('change', $stat); - } - break; - - default: - throw new Horde_ActiveSync_Exception('Unexpected change type in loadPingState'); - } - } - - $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache; $this->savePingState(); } } @@ -381,10 +557,15 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base if (empty($this->_pingState)) { throw new Horde_ActiveSync_Exception('PING state not initialized'); } + /* Update the ping's collection */ + if (!empty($this->_collection)) { + $this->_pingState['collections'][$this->_collection['class']] = $this->_collection; + } + $state = serialize(array('lifetime' => $this->_pingState['lifetime'], 'collections' => $this->_pingState['collections'])); + $query = 'UPDATE ' . $this->_syncDeviceTable . ' SET device_ping = ? WHERE device_id = ?'; - // Need to write to DB - return ;//file_put_contents($this->_stateDir . '/' . $this->_devId, $state); + return $this->_db->update($query, array($state, $this->_devId)); } /** @@ -419,57 +600,92 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base public function getChanges($flags = 0) { $cutoffdate = self::_getCutOffDate(!empty($this->_collection['filtertype']) ? $this->_collection['filtertype'] : 0); - + $this->_thisSyncTS = time(); if (!empty($this->_collection['id'])) { $folderId = $this->_collection['id']; - $this->_logger->debug('Initializing message diff engine'); - + $this->_logger->debug('[' . $this->_devId . '] Initializing message diff engine for ' . $this->_collection['id']); //do nothing if it is a dummy folder if ($folderId != Horde_ActiveSync::FOLDER_TYPE_DUMMY) { /* First, need to see if we have exising changes left over * from a previous sync that resulted in a MORE_AVAILABLE */ - if (!$empty($this->_changes)) { + if (!empty($this->_changes) && count($this->_changes)) { + $this->_logger->debug('[' . $this->_devId . '] Returning previously found changes.'); return $this->_changes; } /* No existing changes, poll the backend */ - $this->_thisSyncTS = time(); - $this->_changes = $this->_backend->getServerChanges($folderId, $this->_lastSyncTS, $this->_thisSyncTS); + $changes = $this->_backend->getServerChanges($folderId, $this->_lastSyncTS, $this->_thisSyncTS, $cutoffdate); + } + $this->_logger->debug('[' . $this->_devId . '] Found ' . count($changes) . ' message changes, checking for PIM initiated changes.'); + $this->_changes = array(); + foreach ($changes as $change) { + $stat = $this->_backend->statMessage($folderId, $change['id']); + $ts = $this->_getPIMChangeTS($change['id']); + $this->_logger->debug('[' . $this->_devId . '] Checking change for ' . $change['id'] . '(PIM TS: ' . $ts . ' Stat TS: ' . $stat['mod']); + if ($ts && $ts >= $stat['mod']) { + $this->_logger->debug('[' . $this->_devId . '] Ignoring PIM initiated change for ' . $change['id'] . '(PIM TS: ' . $ts . ' Stat TS: ' . $stat['mod']); + } else { + $this->_changes[] = $change; + } } - $this->_logger->debug('Found ' . count($this->_changes) . ' message changes'); - } else { - - $this->_logger->debug('Initializing folder diff engine'); - $this->_thisSyncTS = time(); + $this->_logger->debug('[' . $this->_devId . '] Initializing folder diff engine'); $folderlist = $this->_backend->getFolderList(); if ($folderlist === false) { return false; } + $this->_changes = $this->_getDiff($this->_state, $folderlist); + $this->_logger->debug('[' . $this->_devId . '] Found ' . count($this->_changes) . ' folder changes'); + } - if (!isset($syncState) || !$syncState) { - $syncState = array(); - } + return $this->_changes; + } + + /** + * Get a timestamp from the map table for the last PIM-initiated change for + * the provided uid. Used to avoid mirroring back changes to the PIM that it + * originated. + * + * @param string $uid + */ + protected function _getPIMChangeTS($uid) + { + $sql = 'SELECT sync_modtime FROM ' . $this->_syncMapTable . ' WHERE message_uid = ? AND sync_devid = ?'; + try { + return $this->_db->selectValue($sql, array($uid, $this->_devId)); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + } - $this->_changes = $this->_getDiff($syncState, $folderlist); - $this->_logger->debug('Config: Found ' . count($this->_changes) . ' folder changes'); + protected function _getLastSyncTS($syncKey = 0) + { + $sql = 'SELECT MAX(sync_time) FROM ' . $this->_syncStateTable . ' WHERE sync_folderid = ? AND sync_devid = ?'; + $values = array($this->_collection['id'], $this->_devId); + if (!empty($syncKey)) { + $sql .= ' AND sync_key = ?'; + array_push($values, $syncKey); + } + $this->_logger->debug('SQL Query by Horde_ActiveSync_State: ' . $sql . ' VALUES: ' . print_r($values, true)); + try { + $this->_lastSyncTS = $this->_db->selectValue($sql, $values); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); } - return $this->_changes; + return !empty($this->_lastSyncTS) ? $this->_lastSyncTS : 0; } public function getChangeCount() { if (!isset($this->_changes)) { $this->getChanges(); - //throw new Horde_ActiveSync_Exception('Changes not yet retrieved. Must call getChanges() first'); } return count($this->_changes); } /** - * Garbage collector - clean up from previous sync - * requests. + * Garbage collector - clean up from previous sync requests. * * @params string $syncKey The sync key * @@ -484,112 +700,115 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base $guid = $matches[1]; $n = $matches[2]; - $dir = opendir($this->_stateDir); - if (!$dir) { - return false; - } - while ($entry = readdir($dir)) { - if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) { + $sql = 'SELECT sync_key FROM ' . $this->_syncStateTable . ' WHERE sync_devid = ? AND sync_folderid = ?'; + $results = $this->_db->selectAll($sql, array($this->_devId, !empty($this->_collection['id']) ? $this->_collection['id'] : 0)); + $remove = array(); + $guids = array($guid); + foreach ($results as $oldkey) { + if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $oldkey['sync_key'], $matches)) { if ($matches[1] == $guid && $matches[2] < $n) { - unlink($this->_stateDir . '/' . $entry); + $remove[] = $oldkey['sync_key']; } + } else { + /* stale key from previous key series */ + $remove[] = $oldkey['sync_key']; + $guids[] = $matches[1]; } } + if (count($remove)) { + $sql = 'DELETE FROM ' . $this->_syncStateTable . ' WHERE sync_key IN (' . str_repeat('?,', count($remove) - 1) . '?)'; + $this->_db->delete($sql, $remove); + } + $sql = 'SELECT sync_key FROM ' . $this->_syncMapTable . ' WHERE sync_devid = ?'; + $maps = $this->_db->selectValues($sql, array($this->_devId)); + foreach ($maps as $key) { + if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $key, $matches)) { + if ($matches[1] == $guid && $matches[2] < $n) { + $remove[] = $key; + } + } + } + if (count($remove)) { + $sql = 'DELETE FROM ' . $this->_syncMapTable . ' WHERE sync_key IN (' . str_repeat('?,', count($remove) - 1) . '?)'; + $this->_db->delete($sql, $remove); + } return true; } /** - * - * @param $old - * @param $new - * @return unknown_type + * Reset the sync state for this device. */ - private function _getDiff($old, $new) + protected function _resetDeviceState($id) { - $changes = array(); - - // Sort both arrays in the same way by ID - usort($old, array(__CLASS__, 'RowCmp')); - usort($new, array(__CLASS__, 'RowCmp')); - - $inew = 0; - $iold = 0; + $this->_logger->debug('[' . $this->_devId . '] Resetting device state.'); + $state_query = 'DELETE FROM ' . $this->_syncStateTable . ' WHERE sync_devid = ? AND sync_folderid = ?'; + $map_query = 'DELETE FROM ' . $this->_syncMapTable . ' WHERE sync_devid = ? AND sync_folderid = ?'; + try { + $this->_db->delete($state_query, array($this->_devId, $id)); + $this->_db->delete($map_query, array($this->_devId, $id)); + } catch (Horde_Db_Exception $e) { + throw new Horde_ActiveSync_Exception($e); + } + } - // Get changes by comparing our list of messages with - // our previous state - while (1) { - $change = array(); + /** + * Obtain the current policy key, if it exists. + * + * @param string $devId The device id to obtain policy key for. + * + * @return integer The current policy key for this device, or 0 if none + * exists. + */ + public function getPolicyKey($devId) + { - if ($iold >= count($old) || $inew >= count($new)) { - break; - } + } - if ($old[$iold]['id'] == $new[$inew]['id']) { - // Both messages are still available, compare flags and mod - if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) { - // Flags changed - $change['type'] = 'flags'; - $change['id'] = $new[$inew]['id']; - $change['flags'] = $new[$inew]['flags']; - $changes[] = $change; - } + /** + * Save a new device policy key to storage. + * + * @param string $devId The device id + * @param integer $key The new policy key + */ + public function setPolicyKey($devId, $key) + { - if ($old[$iold]['mod'] != $new[$inew]['mod']) { - $change['type'] = 'change'; - $change['id'] = $new[$inew]['id']; - $changes[] = $change; - } + } - $inew++; - $iold++; - } else { - if ($old[$iold]['id'] > $new[$inew]['id']) { - // Message in state seems to have disappeared (delete) - $change['type'] = 'delete'; - $change['id'] = $old[$iold]['id']; - $changes[] = $change; - $iold++; - } else { - // Message in new seems to be new (add) - $change['type'] = 'change'; - $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE; - $change['id'] = $new[$inew]['id']; - $changes[] = $change; - $inew++; - } - } - } + /** + * Return a device remotewipe status + * + * @param string $devId The device id + * + * @return int + */ + public function getDeviceRWStatus($devId) + { - while ($iold < count($old)) { - // All data left in _syncstate have been deleted - $change['type'] = 'delete'; - $change['id'] = $old[$iold]['id']; - $changes[] = $change; - $iold++; - } + } - while ($inew < count($new)) { - // All data left in new have been added - $change['type'] = 'change'; - $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE; - $change['id'] = $new[$inew]['id']; - $changes[] = $change; - $inew++; - } + /** + * Set a new remotewipe status for the device + * + * @param string $devid + * @param string $status + * + * @return boolean + */ + public function setDeviceRWStatus($devid, $status) + { - return $changes; } - /** + /** + * Explicitly remove a state from storage. * - * @param $a - * @param $b - * @return unknown_type + * @param string $synckey */ - static public function RowCmp($a, $b) + public function removeState($synckey) { - return $a['id'] < $b['id'] ? 1 : -1; + $this->_resetDeviceState(); } } \ No newline at end of file diff --git a/framework/ActiveSync/package.xml b/framework/ActiveSync/package.xml index b748d973b..5302062df 100644 --- a/framework/ActiveSync/package.xml +++ b/framework/ActiveSync/package.xml @@ -38,6 +38,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -102,6 +103,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + diff --git a/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php b/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php index 1ba54e356..1265440ad 100644 --- a/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php +++ b/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php @@ -1,5 +1,5 @@ @@ -25,7 +25,7 @@ class Horde_ActiveSync_FileStateTest extends Horde_Test_Case ->method('contacts_list') ->will($this->returnValue($fixture['contacts_list'])); - $connector->expects($this->exactly(1)) + $connector->expects($this->exactly(2)) ->method('contacts_getActionTimestamp') ->will($this->returnValue($fixture['contacts_getActionTimestamp'])); @@ -37,12 +37,12 @@ class Horde_ActiveSync_FileStateTest extends Horde_Test_Case 'class' => 'Contacts')); $state->loadState(0); - + /* Get the current state from the "server" */ $changes = $state->getChanges(); $this->assertEquals(1, $state->getChangeCount()); $this->assertEquals(array('type' => 'change', 'flags' => 'NewMessage', 'id' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com'), $changes[0]); - + /* Import the state into the state object */ foreach($changes as $change) { // We know it's always a 'change' since the above test passed @@ -77,7 +77,7 @@ class Horde_ActiveSync_FileStateTest extends Horde_Test_Case $this->markTestIncomplete(); return; } - + public function testConflicts() { $this->markTestIncomplete(); diff --git a/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php b/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php index 78e2b1b8d..5f87fc0e1 100644 --- a/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php +++ b/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php @@ -46,7 +46,7 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case // Events fixture - only need the uid property for this test $e1 = new stdClass(); $e1->uid = '20080112030603.249j42k3k068@test.theupstairsroom.com'; - + // Test Contacts - simulates returning two contacts, both of which have no history modify entries. $fixture = array('contacts_list' => array('20070112030603.249j42k3k068@test.theupstairsroom.com', '20070112030611.62g1lg5nry80@test.theupstairsroom.com'), @@ -58,7 +58,7 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case 'tasks_list' => array('20070112030603.249j42k3k068@test.theupstairsroom.com', '20070112030611.62g1lg5nry80@test.theupstairsroom.com'), 'tasks_getActionTimestamp' => 0); - + /* Mock the registry responses */ $connector = $this->getMockSkipConstructor('Horde_ActiveSync_Driver_Horde_Connector_Registry'); $connector->expects($this->once())->method('contacts_list')->will($this->returnValue($fixture['contacts_list'])); @@ -232,7 +232,8 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case } catch (Horde_ActiveSync_Exception $e) { $this->fail($e->getMessage()); } - $this->assertEquals(array('id' => 'localhost@123.123', 'mod' => 0, 'flags' => 1), $results); + $this->assertEquals('localhost@123.123', $results['id']); + $this->assertEquals(1, $results['flags']); /* Try editing a contact */ try { @@ -240,7 +241,8 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case } catch (Horde_ActiveSync_Exception $e) { $this->fail($e->getMessage()); } - $this->assertEquals(array('id' => 'localhost@123.123', 'mod' => 0, 'flags' => 1), $results); + $this->assertEquals('localhost@123.123', $results['id']); + $this->assertEquals(1, $results['flags']); /* Try adding a new appointment */ $message = new Horde_ActiveSync_Message_Appointment(); @@ -249,7 +251,8 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case } catch (Horde_ActiveSync_Exception $e) { $this->fail($e->getMessage()); } - $this->assertEquals(array('id' => 'localhost@123.123', 'mod' => 0, 'flags' => 1), $results); + $this->assertEquals('localhost@123.123', $results['id']); + $this->assertEquals(1, $results['flags']); /* Try editing an appointment */ try { @@ -257,7 +260,8 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case } catch (Horde_ActiveSync_Exception $e) { $this->fail($e->getMessage()); } - $this->assertEquals(array('id' => 'localhost@123.123', 'mod' => 0, 'flags' => 1), $results); + $this->assertEquals('localhost@123.123', $results['id']); + $this->assertEquals(1, $results['flags']); /* Try adding a new task */ $message = new Horde_ActiveSync_Message_Task(); @@ -266,7 +270,8 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case } catch (Horde_ActiveSync_Exception $e) { $this->fail($e->getMessage()); } - $this->assertEquals(array('id' => 'localhost@123.123', 'mod' => 0, 'flags' => 1), $results); + $this->assertEquals('localhost@123.123', $results['id']); + $this->assertEquals(1, $results['flags']); /* Try editing an appointment */ try { @@ -274,7 +279,12 @@ class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case } catch (Horde_ActiveSync_Exception $e) { $this->fail($e->getMessage()); } - $this->assertEquals(array('id' => 'localhost@123.123', 'mod' => 0, 'flags' => 1), $results); + + /* Only check these two fields, 'mod' will contain the timestamp the + * change was actually made. + */ + $this->assertEquals('localhost@123.123', $results['id']); + $this->assertEquals(1, $results['flags']); } /**