Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 139 additions & 8 deletions lib/Horde/ActiveSync/Collections.php
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,8 @@ public function checkStaleRequest()
*/
public function haveHierarchy()
{
return isset($this->_cache->hierarchy);
return !empty($this->_cache->hierarchy)
&& $this->_cache->hierarchy !== '0';
}

/**
Expand Down Expand Up @@ -1163,12 +1164,33 @@ public function pollForChanges($heartbeat, $interval, array $options = [])
)
);

// 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;
if (!empty($options['pingable'])) {
if ($this->collectionsNeedFolderResync()) {
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
}

$this->restorePingableCollectionsFromCache();

// 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 (!$this->havePingableCollections()) {
$this->_logger->err('COLLECTIONS: No pingable collections.');
if ($this->_as->device->version >= Horde_ActiveSync::VERSION_TWELVEONE
&& !$this->haveHierarchy()) {
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
}

return self::COLLECTION_ERR_SERVER;
}
}

if ($this->_as->device->version >= Horde_ActiveSync::VERSION_TWELVEONE
&& !$this->haveHierarchy()) {
$this->_logger->info(
'COLLECTIONS: Hierarchy sync required, terminating pollForChanges.'
);
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
}

// Need to update AND SAVE the timestamp for race conditions to be
Expand Down Expand Up @@ -1218,9 +1240,22 @@ public function pollForChanges($heartbeat, $interval, array $options = [])
try {
$this->initCollectionState($collection, true);
} catch (Horde_ActiveSync_Exception_StateGone $e) {
if (!empty($options['pingable'])) {
$this->_logger->notice(
sprintf(
'COLLECTIONS: State not found for %s during PING; dropping orphan collection from cache.',
$id
)
);
$this->_cache->removePingableCollection($id);
$this->_cache->removeCollection($id, true);
unset($this->_collections[$id]);
$this->save();
continue;
}
$this->_logger->notice(
sprintf(
'COLLECTIONS: State not found for %s. Continuing by rquesting a SYNC.',
'COLLECTIONS: State not found for %s. Continuing by requesting a SYNC.',
$id
)
);
Expand Down Expand Up @@ -1386,13 +1421,109 @@ public function havePingableCollections()
return false;
}

/**
* True if a collection has item sync state but no folder cache entry.
*
* The client must run FolderSync to remap folder ids (common after a
* partial cache reset or hierarchy invalidation).
*
* @return boolean
*/
public function collectionsNeedFolderResync()
{
foreach ($this->_cache->getCollections(false) as $id => $collection) {
$synckey = $collection['synckey'] ?? '';
if ($synckey === '' && !empty($collection['lastsynckey'])) {
$synckey = $collection['lastsynckey'];
}
if ($synckey === '' || $synckey === '0') {
continue;
}
if (!$this->_cache->getFolder($id)) {
$this->_logger->info(
sprintf(
'COLLECTIONS: Collection %s has sync state but no folder cache entry; FolderSync required.',
$id
)
);

return true;
}
}

return false;
}

/**
* Re-mark collections that have synckeys as PINGable.
*
* Outlook and similar clients may send explicit PING folder lists before
* synckeys exist; a previous request must not leave the device with no
* pingable collections until the user resets the account.
*
* @return boolean True if the sync cache was updated.
*/
public function restorePingableCollectionsFromCache()
{
$updated = false;

foreach ($this->_cache->getCollections(false) as $id => $collection) {
$synckey = $collection['synckey'] ?? '';
if ($synckey === '' && !empty($collection['lastsynckey'])) {
$synckey = $collection['lastsynckey'];
}
if ($synckey === '' || $synckey === '0' || !$this->_cache->getFolder($id)) {
continue;
}

if (!$this->_cache->collectionIsPingable($id)) {
$this->_cache->setPingableCollection($id);
$updated = true;
}

if (!isset($this->_collections[$id])) {
if (empty($collection['class'])) {
try {
$collection['class'] = $this->getCollectionClass($id);
} catch (Horde_ActiveSync_Exception $e) {
continue;
}
}
if (empty($collection['serverid'])) {
try {
$collection['serverid'] = $this->getBackendIdForFolderUid($id);
} catch (Horde_ActiveSync_Exception $e) {
continue;
}
}
if (empty($collection['synckey']) && !empty($collection['lastsynckey'])) {
$collection['synckey'] = $collection['lastsynckey'];
}
$this->_collections[$id] = $collection;
}
}

if ($updated) {
$this->save();
}

return $updated;
}

/**
* Marks all loaded collections with a synckey as pingable.
*/
public function updatePingableFlag()
{
if (!count($this->_collections)) {
return;
}

$collections = $this->_cache->getCollections(false);
foreach ($collections as $id => $collection) {
if (!isset($this->_collections[$id])) {
continue;
}
if (!empty($this->_collections[$id]['synckey'])) {
$this->_logger->meta(
sprintf(
Expand Down
72 changes: 63 additions & 9 deletions lib/Horde/ActiveSync/Request/Ping.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,22 @@ protected function _handle()
$this->_logger->info('Handling empty PING request.');
$isEmpty = true;
$collections->loadCollectionsFromCache();
$collections->restorePingableCollectionsFromCache();
if ($collections->collectionCount() == 0
|| !$collections->havePingableCollections()) {
$this->_logger->warn('Empty PING request with no cached collections. Request full PING.');
$this->_statusCode = self::STATUS_MISSING;
if ($collections->collectionsNeedFolderResync()
|| ($this->_device->version >= Horde_ActiveSync::VERSION_TWELVEONE
&& !$collections->haveHierarchy())) {
$this->_logger->info(
'Empty PING with stale folder or hierarchy state; requesting FolderSync.'
);
$this->_statusCode = self::STATUS_FOLDERSYNCREQD;
} else {
$this->_logger->warn(
'Empty PING request with no cached collections. Request full PING.'
);
$this->_statusCode = self::STATUS_MISSING;
}
$this->_handleGlobalError();
return true;
}
Expand All @@ -162,6 +174,7 @@ protected function _handle()
}
$this->_logger->meta(sprintf('Actual heartbeat value in use is %s.', $heartbeat));
if ($this->_decoder->getElementStartTag(self::FOLDERS)) {
$pingCollectionsLoaded = false;
while ($this->_decoder->getElementStartTag(self::FOLDER)) {
$collection = [];
if ($this->_decoder->getElementStartTag(self::SERVERENTRYID)) {
Expand All @@ -181,14 +194,43 @@ protected function _handle()
// iOS clients that request collections in PING before
// they issue an initial SYNC for them.
$collections->addCollection($collection, true);
$pingCollectionsLoaded = true;
} catch (Horde_ActiveSync_Exception_StateGone $e) {
}
}

// Since PING sends all or none (no PARTIAL) we update the
// pingable flags so we have it for an empty PING.
$collections->validateFromCache();
$collections->updatePingableFlag();
if ($pingCollectionsLoaded) {
$collections->validateFromCache();
$collections->updatePingableFlag();
} else {
// Outlook and others may list folders in PING before a
// synckey exists; do not clear pingable flags on cached
// collections. Fall back to the last known PING set.
$this->_logger->info(
'No collections loaded from explicit PING folder list; using cached collections.'
);
$collections->loadCollectionsFromCache();
$collections->restorePingableCollectionsFromCache();
if ($collections->collectionCount() == 0) {
if ($collections->collectionsNeedFolderResync()
|| ($this->_device->version >= Horde_ActiveSync::VERSION_TWELVEONE
&& !$collections->haveHierarchy())) {
$this->_logger->info(
'Explicit PING with stale folder or hierarchy state; requesting FolderSync.'
);
$this->_statusCode = self::STATUS_FOLDERSYNCREQD;
} else {
$this->_logger->warn(
'Explicit PING request with no loadable collections.'
);
$this->_statusCode = self::STATUS_MISSING;
}
$this->_handleGlobalError();
return true;
}
}

if (!$this->_decoder->getElementEndTag()) {
throw new Horde_ActiveSync_Exception('Protocol Error');
Expand All @@ -210,6 +252,16 @@ protected function _handle()

// Start waiting for changes, but only if we don't have any errors
if ($this->_statusCode == self::STATUS_NOCHANGES) {
if ($this->_device->version >= Horde_ActiveSync::VERSION_TWELVEONE
&& !$collections->haveHierarchy()) {
$this->_logger->info(
'No HIERARCHY SYNCKEY in sync_cache during PING, requesting FolderSync.'
);
$this->_statusCode = self::STATUS_FOLDERSYNCREQD;
$this->_handleGlobalError();
return true;
}

$changes = $collections->pollForChanges($heartbeat, $interval, ['pingable' => true]);
if ($changes !== true && $changes !== false) {
switch ($changes) {
Expand All @@ -236,12 +288,14 @@ protected function _handle()
if ($this->_device->version < Horde_ActiveSync::VERSION_FOURTEEN) {
$this->_logger->warn('Version is < 14.0, returning false since we have no PINGABLE collections.');
return false;
} else {
$this->_logger->warn('Version is >= 14.0 returning status code 132 since we have no PINGABLE collections.');
$this->_statusCode = Horde_ActiveSync_Status::STATEFILE_NOT_FOUND;
$this->_handleGlobalError();
return true;
}

$this->_logger->warn(
'No PINGABLE collections; requesting FolderSync instead of status 132.'
);
$this->_statusCode = self::STATUS_FOLDERSYNCREQD;
$this->_handleGlobalError();
return true;
}
} elseif ($changes) {
$collections->save(true);
Expand Down
Loading