/** * Get a list of server changes that occured during the specified time * period. * * @param Horde_ActiveSync_Folder_Base $folder * The ActiveSync folder object to request changes for. * @param integer $from_ts The starting timestamp * @param integer $to_ts The ending timestamp * @param integer $cutoffdate The earliest date to retrieve back to. * * @param boolean $ping If true, returned changeset may * not contain the full changeset, but rather * only a single change, designed only to * indicate *some* change has taken place. The * value should not be used to determine *what* * change has taken place. * @param boolean $ignoreFirstSync If true, will not trigger an initial sync * if $from_ts is 0. Needed to avoid race * conditions when we don't have any history * data. @since 2.6.0 * @todo If we can pass the synckey ( * perhaps as part of $folder), we can * just look for synckey 0 to know when * we CAN trigger an initial sync without * this flag. * @param integer $maxitems Maximum number of recipients for a RI * collection. @since 2.12.0 * @param boolean $refreshFilter Force a SOFTDELETE operation and check * for new items within the (newly changed) * current FilterType interval. * @since 2.19.0 * * @return array An array of hashes that contain the ids of items that have * changed in the specified collection along with a 'type' * flag that indicates the type of change. * @todo H6 - Clean up method parameters, update parent class etc... * - Return a new ids object. * - Refactor to use a Repository pattern for each supported * collection and move the bulk of the logic in the switch * structure below to the various classes - and refactor out most * of the stuff in the registry connector since the Repositories * will handle the basic CRUD operations and change detection on * each collection. */ public function getServerChanges($folder, $from_ts, $to_ts, $cutoffdate, $ping, $ignoreFirstSync = false, $maxitems = 100, $refreshFilter = false) { $this->_logger->info(sprintf("[%s] Horde_Core_ActiveSync_Driver::getServerChanges(%s, %u, %u, %u, %d, %s, %u, %s)", $this->_pid, $folder->serverid(), $from_ts, $to_ts, $cutoffdate, $ping, $ignoreFirstSync, $maxitems, $refreshFilter)); $changes = array('add' => array(), 'delete' => array(), 'modify' => array(), 'soft' => array()); ob_start(); switch ($folder->collectionClass()) { case Horde_ActiveSync::CLASS_CALENDAR: if ($folder->serverid() == self::APPOINTMENTS_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } if ($from_ts == 0 && !$ignoreFirstSync) { // Can't use History if it's a first sync $startstamp = (int) $cutoffdate; $endstamp = time() + 32140800; //60 * 60 * 24 * 31 * 12 == one year try { $changes['add'] = $this->_connector->calendar_listUids($startstamp, $endstamp, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } $folder->setSoftDeleteTimes($cutoffdate, time()); } else { try { $changes = $this->_connector->getChanges('calendar', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } // Softdelete. We check this once per close-to 24 hour period,or // if the FILTERTYPE range changes. We introduce an element of // randomness in the time to help avoid a large number of // clients performing a softdelete at once. It's 23 hours + some // random number of minutes < 60. // // @todo We need to populate additional events if the FILTERTYPE // is expanded, but we need to persist the previous FILTERTYPE // so we know exactly which events we already have and which // we don't (since we don't track the UIDs themselves). if (!$ping) { if (!$refreshFilter) { $sd = $folder->getSoftDeleteTimes(); if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) { $from = $sd[2]; } } else { // Force refresh the FILTERTYPE. $from = 0; } if (isset($from)) { $this->_logger->info(sprintf('[%s] Polling for SOFTDELETE items in calendar collection', getmypid())); // Gets the list of ids contained between the last softdelete // run and the current cutoffdate. $changes['soft'] = $this->_connector->softDelete('calendar', $from, $cutoffdate, $server_id); $folder->setSoftDeleteTimes($cutoffdate, time()); } } } break; case Horde_ActiveSync::CLASS_CONTACTS: // Multiplexed or multiple? if ($folder->serverid() == self::CONTACTS_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } // Can't use History for first sync if ($from_ts == 0 && !$ignoreFirstSync) { try { $changes['add'] = $this->_connector->contacts_listUids($server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { try { $changes = $this->_connector->getChanges('contacts', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } break; case Horde_ActiveSync::CLASS_TASKS: // Multiplexed or multiple? if ($folder->serverid() == self::TASKS_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } // Can't use History for first sync if ($from_ts == 0 && !$ignoreFirstSync) { try { $changes['add'] = $this->_connector->tasks_listUids($server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { try { $changes = $this->_connector->getChanges('tasks', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } break; case Horde_ActiveSync::CLASS_NOTES: // Multiplexed or multiple? if ($folder->serverid() == self::NOTES_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } // Can't use History for first sync if ($from_ts == 0 && !$ignoreFirstSync) { try { $changes['add'] = $this->_connector->notes_listUids($server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { try { $changes = $this->_connector->getChanges('notes', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } break; case 'RI': $folder->setChanges($this->_connector->getRecipientCache($maxitems)); $changes = array('add' => $folder->added(), 'delete' => $folder->removed(), 'modify' => array(), 'soft' => array()); break; case Horde_ActiveSync::CLASS_EMAIL: if (empty($this->_imap)) { $this->_endBuffer(); return array(); } $this->_logger->info(sprintf('[%s] %s IMAP PREVIOUS MODSEQ: %d', $this->_pid, $folder->serverid(), $folder->modseq())); if ($ping) { try { $ping_res = $this->_imap->ping($folder); if ($ping_res) { $changes['add'] = array(1); } } catch (Horde_ActiveSync_Exeption_StaleState $e) { $this->_endBuffer(); throw $e; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { // SOFTDELETE, but only if we aren't refreshing FILTERTYPE. $soft = false; if (!$refreshFilter) { $sd = $folder->getSoftDeleteTimes(); if (empty($sd[0]) && empty($sd[1]) && !empty($cutoffdate)) { // No SOFTDELETE performed, this is likely the first // sync so we must prime the SOFTDELETE values. $folder->setSoftDeleteTimes((int) $cutoffdate, time()); } else { if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) { $soft = true; } else { $soft = false; } } } try { $folder = $this->_imap->getMessageChanges($folder, array('sincedate' => (int) $cutoffdate, 'protocolversion' => $this->_version, 'softdelete' => $soft, 'refreshfilter' => $refreshFilter)); // Poll the maillog for reply/forward state changes. if (empty($GLOBALS['conf']['activesync']['no_maillogsync'])) { $folder = $this->_getMaillogChanges($folder, $from_ts); } } catch (Horde_ActiveSync_Exception_StaleState $e) { $this->_endBuffer(); throw $e; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } $changes['add'] = $folder->added(); $changes['delete'] = $folder->removed(); $changes['modify'] = $folder->changed(); $changes['soft'] = $folder->getSoftDeleted(); } } $results = array(); if (!$folder->haveInitialSync) { $this->_logger->info(sprintf('[%s] Initial sync, only sending UID.', $this->_pid)); $this->_endBuffer(); $add = $changes['add']; $changes = null; return $add; } else { foreach ($changes['add'] as $add) { $results[] = array('id' => $add, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE, 'flags' => Horde_ActiveSync::FLAG_NEWMESSAGE); } } // For CLASS_EMAIL, all changes are a change in flags, categories or // softdelete. if ($folder->collectionClass() == Horde_ActiveSync::CLASS_EMAIL) { $flags = $folder->flags(); $categories = $folder->categories(); foreach ($changes['modify'] as $uid) { $results[] = array('id' => $uid, 'type' => Horde_ActiveSync::CHANGE_TYPE_FLAGS, 'flags' => $flags[$uid], 'categories' => $categories[$uid]); } } else { foreach ($changes['modify'] as $change) { $results[] = array('id' => $change, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE); } } // Server Deletions and Softdeletions foreach ($changes['delete'] as $deleted) { $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_DELETE); } if (!empty($changes['soft'])) { foreach ($changes['soft'] as $deleted) { $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_SOFTDELETE); } } $this->_endBuffer(); return $results; }
/** * Get a list of server changes that occured during the specified time * period. * * @param Horde_ActiveSync_Folder_Base $folder * The ActiveSync folder object to request changes for. * @param integer $from_ts The starting timestamp * @param integer $to_ts The ending timestamp * @param integer $cutoffdate The earliest date to retrieve back to. * * @param boolean $ping If true, returned changeset may * not contain the full changeset, but rather * only a single change, designed only to * indicate *some* change has taken place. The * value should not be used to determine *what* * change has taken place. * @param boolean $ignoreFirstSync If true, will not trigger an initial sync * if $from_ts is 0. Needed to avoid race * conditions when we don't have any history * data. @since 2.6.0 * @param integer $maxitems Maximum number of recipients for a RI * collection. @since 2.12.0 * * @return array An array of hashes that contain the ids of items that have * changed in the specified collection along with a 'type' * flag that indicates the type of change. * @todo H6 - clean up method parameters, update parent class etc... * return a new ids object. */ public function getServerChanges($folder, $from_ts, $to_ts, $cutoffdate, $ping, $ignoreFirstSync = false, $maxitems = 100) { $this->_logger->info(sprintf("[%s] Horde_Core_ActiveSync_Driver::getServerChanges(%s, %u, %u, %u, %d)", $this->_pid, $folder->serverid(), $from_ts, $to_ts, $cutoffdate, $ping)); $changes = array('add' => array(), 'delete' => array(), 'modify' => array(), 'soft' => array()); ob_start(); switch ($folder->collectionClass()) { case Horde_ActiveSync::CLASS_CALENDAR: if ($folder->serverid() == self::APPOINTMENTS_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } if ($from_ts == 0 && !$ignoreFirstSync) { // Can't use History if it's a first sync $startstamp = (int) $cutoffdate; $endstamp = time() + 32140800; //60 * 60 * 24 * 31 * 12 == one year try { $changes['add'] = $this->_connector->calendar_listUids($startstamp, $endstamp, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } $folder->setSoftDeleteTimes($cutoffdate, time()); } else { try { $changes = $this->_connector->getChanges('calendar', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } // Softdelete. We check this once per close-to 24 hour period. // We introduce an element of randomness in the time to help // avoid a large number of clients performing a softdelete at // once. It's 23 hours + some random number of minutes < 60. if (!$ping) { $sd = $folder->getSoftDeleteTimes(); if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) { $this->_logger->info(sprintf('[%s] Polling for SOFTDELETE items in calendar collection', getmypid())); // Gets the list of ids contained between the last softdelete // run and the current cutoffdate. $changes['soft'] = $this->_connector->softDelete('calendar', $sd[0], $cutoffdate, $server_id); $folder->setSoftDeleteTimes($cutoffdate, time()); } } } break; case Horde_ActiveSync::CLASS_CONTACTS: // Multiplexed or multiple? if ($folder->serverid() == self::CONTACTS_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } // Can't use History for first sync if ($from_ts == 0 && !$ignoreFirstSync) { try { $changes['add'] = $this->_connector->contacts_listUids($server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { try { $changes = $this->_connector->getChanges('contacts', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } break; case Horde_ActiveSync::CLASS_TASKS: // Multiplexed or multiple? if ($folder->serverid() == self::TASKS_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } // Can't use History for first sync if ($from_ts == 0 && !$ignoreFirstSync) { try { $changes['add'] = $this->_connector->tasks_listUids($server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { try { $changes = $this->_connector->getChanges('tasks', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } break; case Horde_ActiveSync::CLASS_NOTES: // Multiplexed or multiple? if ($folder->serverid() == self::NOTES_FOLDER_UID) { $server_id = null; } else { $parts = $this->_parseFolderId($folder->serverid()); $server_id = $parts[self::FOLDER_PART_ID]; } // Can't use History for first sync if ($from_ts == 0 && !$ignoreFirstSync) { try { $changes['add'] = $this->_connector->notes_listUids($server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { try { $changes = $this->_connector->getChanges('notes', $from_ts, $to_ts, $server_id); } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } break; case 'RI': $folder->setChanges($this->_connector->getRecipientCache($maxitems)); $changes = array('add' => $folder->added(), 'delete' => $folder->removed(), 'modify' => array(), 'soft' => array()); break; case Horde_ActiveSync::CLASS_EMAIL: if (empty($this->_imap)) { $this->_endBuffer(); return array(); } $this->_logger->info(sprintf('[%s] %s IMAP MODSEQ: %d', $this->_pid, $folder->serverid(), $folder->modseq())); if ($ping) { try { $ping_res = $this->_imap->ping($folder); if ($ping_res) { $changes['add'] = array(1); } } catch (Horde_ActiveSync_Exeption_StaleState $e) { $this->_endBuffer(); throw $e; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } } else { // Calculate SOFTDELETE if needed? $sd = $folder->getSoftDeleteTimes(); if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) { $soft = true; } else { $soft = false; } try { $folder = $this->_imap->getMessageChanges($folder, array('sincedate' => (int) $cutoffdate, 'protocolversion' => $this->_version, 'softdelete' => $soft)); // Poll the maillog for reply/forward state changes. $folder = $this->_getMaillogChanges($folder, $from_ts); } catch (Horde_ActiveSync_Exception_StaleState $e) { $this->_endBuffer(); throw $e; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception_AuthenticationFailure $e) { $this->_endBuffer(); throw $e; } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); $this->_endBuffer(); return array(); } $changes['add'] = $folder->added(); $changes['delete'] = $folder->removed(); $changes['modify'] = $folder->changed(); $changes['soft'] = $folder->getSoftDeleted(); } } $results = array(); foreach ($changes['add'] as $add) { $results[] = array('id' => $add, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE, 'flags' => Horde_ActiveSync::FLAG_NEWMESSAGE); } // For CLASS_EMAIL, all changes are a change in flags, categories or // softdelete. if ($folder->collectionClass() == Horde_ActiveSync::CLASS_EMAIL) { $flags = $folder->flags(); $categories = $folder->categories(); foreach ($changes['modify'] as $uid) { $results[] = array('id' => $uid, 'type' => Horde_ActiveSync::CHANGE_TYPE_FLAGS, 'flags' => $flags[$uid], 'categories' => $categories[$uid]); } } else { foreach ($changes['modify'] as $change) { $results[] = array('id' => $change, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE); } } // Server Deletions and Softdeletions foreach ($changes['delete'] as $deleted) { $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_DELETE); } if (!empty($changes['soft'])) { foreach ($changes['soft'] as $deleted) { $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_SOFTDELETE); } } $this->_endBuffer(); return $results; }