From 364e3e23989905ca4bf828ecb924825e30d9b866 Mon Sep 17 00:00:00 2001 From: Torben Dannhauer Date: Wed, 9 Sep 2026 18:59:08 +0200 Subject: [PATCH 1/2] fix(activesync): do not buffer Search keep-alive output Skip ob_start around mailbox search when a progress callback is present so WBXML keep-alives reach the client. Exclude the callback from the search cache key. --- lib/Horde/Core/ActiveSync/Driver.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/Horde/Core/ActiveSync/Driver.php b/lib/Horde/Core/ActiveSync/Driver.php index 0ec40b98..1996d373 100644 --- a/lib/Horde/Core/ActiveSync/Driver.php +++ b/lib/Horde/Core/ActiveSync/Driver.php @@ -2789,9 +2789,12 @@ public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ { $type = Horde_String::lower($params->type); $rows = null; + $progress = $params->options['progress'] ?? null; + $cacheOptions = $params->options; + unset($cacheOptions['progress']); if ($this->_cache) { - $cache_key = 'HCASD:' . $type . ':' . $GLOBALS['registry']->getAuth() . ':' . hash('md5', serialize([$params->query, $params->deepTraversal, $params->options])); + $cache_key = 'HCASD:' . $type . ':' . $GLOBALS['registry']->getAuth() . ':' . hash('md5', serialize([$params->query, $params->deepTraversal, $cacheOptions])); if ($this->_cache->exists($cache_key, 0)) { if ($params->rebuildResults) { $this->_cache->expire($cache_key); @@ -2802,7 +2805,12 @@ public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ } if ($rows === null) { - ob_start(); + // A progress callback writes WBXML keep-alives to the client; + // capturing stdout would swallow them. + $captureOutput = !is_callable($progress); + if ($captureOutput) { + ob_start(); + } try { switch ($type) { case 'gal': @@ -2824,7 +2832,9 @@ public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ $rows = null; } - $this->_endBuffer(); + if ($captureOutput) { + $this->_endBuffer(); + } if ($rows !== null && $this->_cache) { $this->_cache->set($cache_key, json_encode($rows)); From 498cc4582cc4f9f2ac9aaadb6a621404a8ddefcd Mon Sep 17 00:00:00 2001 From: Torben Dannhauer Date: Thu, 10 Sep 2026 08:42:49 +0200 Subject: [PATCH 2/2] fix(activesync): exclude Search time-budget options from the result cache Do not cache time-truncated mailbox Search results, and omit deadline and stats from the cache key so keep-alive callbacks cannot poison it. --- lib/Horde/Core/ActiveSync/Driver.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Horde/Core/ActiveSync/Driver.php b/lib/Horde/Core/ActiveSync/Driver.php index 1996d373..b04361e9 100644 --- a/lib/Horde/Core/ActiveSync/Driver.php +++ b/lib/Horde/Core/ActiveSync/Driver.php @@ -2791,7 +2791,7 @@ public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ $rows = null; $progress = $params->options['progress'] ?? null; $cacheOptions = $params->options; - unset($cacheOptions['progress']); + unset($cacheOptions['progress'], $cacheOptions['deadline'], $cacheOptions['stats']); if ($this->_cache) { $cache_key = 'HCASD:' . $type . ':' . $GLOBALS['registry']->getAuth() . ':' . hash('md5', serialize([$params->query, $params->deepTraversal, $cacheOptions])); @@ -2837,7 +2837,11 @@ public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ } if ($rows !== null && $this->_cache) { - $this->_cache->set($cache_key, json_encode($rows)); + $truncated = is_object($params->options['stats'] ?? null) + && !empty($params->options['stats']->truncated); + if (!$truncated) { + $this->_cache->set($cache_key, json_encode($rows)); + } } }