From ee6d06c82d552bb6a60b8e04417274f7cb5b4ae6 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Fri, 26 Jun 2026 07:03:12 +0200 Subject: [PATCH] fix: Don't cache outdated/changed prefs in session Changing a pref now invalidates the prefs cache inside the session. Adresses horde/sessionhandler#13 --- lib/Horde/Core/Prefs/Cache/Session.php | 29 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/Horde/Core/Prefs/Cache/Session.php b/lib/Horde/Core/Prefs/Cache/Session.php index ee6d7171..cb6aac25 100644 --- a/lib/Horde/Core/Prefs/Cache/Session.php +++ b/lib/Horde/Core/Prefs/Cache/Session.php @@ -17,10 +17,18 @@ /** * Cache storage implementation using HordeSession. * - * Reads and writes go through the modern PSR-4 {@see HordeSession}. The wire - * format diverges from values previously written through the legacy - * `Horde_Session::set()` path. Stale entries from the prior format are - * treated as missing on the first read after deploy. + * Reads go through the modern PSR-4 {@see HordeSession}. Writes invalidate + * the cached scope instead of replacing it, so the next request reloads the + * scope from storage. This avoids a write-ordering race against + * {@see \Horde\Core\Session\SessionLifecycle::shutdown()}, which mirrors + * {@see HordeSession} back into `$_SESSION` at request shutdown: a + * `store()` that ran after the mirror would never reach the persisted + * session row, leaving subsequent requests with stale prefs until the + * session was destroyed. + * + * The wire format diverges from values previously written through the + * legacy `Horde_Session::set()` path. Stale entries from the prior format + * are treated as missing on the first read after deploy. * * @author Michael Slusarz * @author Ralf Lang @@ -48,13 +56,20 @@ public function get($scope) } /** + * Invalidate the cached scope. + * + * {@see Horde_Prefs::store()} invokes this after a dirty scope has been + * written to storage. Rather than re-serializing the scope into the + * session here — which would race the lifecycle mirror at shutdown and + * could be lost before reaching the session row — drop the slot so the + * next request loads the scope fresh from storage and repopulates the + * cache on a clean read path. */ public function store($scope_ob) { - $this->_session()->setScoped( + $this->_session()->removeScoped( 'horde', - self::SESS_KEY . $this->_params['user'] . '/' . $scope_ob->scope, - $scope_ob + self::SESS_KEY . $this->_params['user'] . '/' . $scope_ob->scope ); }