From 1790449a4395f3ccb56a2ef14cd6a2770fe8eb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20Charles=20Del=C3=A9pine?= Date: Wed, 14 Oct 2026 23:15:49 +0200 Subject: [PATCH] fix(registry): don't re-register default bindings on a provided injector Horde_Registry::__construct() unconditionally runs DefaultInjectorBindings::register() on the injector it ends up with. That is harmless when the registry creates the injector itself, but on the modern stack an injector is already provided and may already hold resolved instances. Registering the defaults again replaces their binders, so the next lookup builds a fresh object. Observed on a route with a minimal middleware stack (/auth/login: EstablishHordeSession, AuthHordeSession, no HordeCore). AuthHordeSession needs a Horde_Registry, which the container auto-wires. The constructor then creates a Horde_Session shim and calls setup(), which resolves SessionLifecycle through $GLOBALS['injector']. That produced a second SessionLifecycle (setupApplied=false) while the native session was already active, so PHP emitted: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active and the same warning for session_cache_limiter(), session_name() and session_set_save_handler(). Two changes: - Register the default bindings only on an injector the registry created itself. A provided injector was bound by whoever provided it; RampageBootstrap registers them before any middleware runs. - Bind Horde_Registry to a small factory that seeds $GLOBALS['injector'] with the container's injector when it is unset, so a registry built from DI shares it instead of minting a disconnected one. This keeps RampageBootstrap free of globals, in line with the goal stated in horde/Core#227. The legacy path is unchanged: with $GLOBALS['injector'] unset the registry still creates and registers its own injector. --- lib/Horde/Registry.php | 9 +++++++-- src/DefaultInjectorBindings.php | 1 + src/Factory/HordeRegistryFactory.php | 30 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/Factory/HordeRegistryFactory.php diff --git a/lib/Horde/Registry.php b/lib/Horde/Registry.php index 75019016..aed061b7 100644 --- a/lib/Horde/Registry.php +++ b/lib/Horde/Registry.php @@ -522,10 +522,15 @@ public function __construct($session_flags = 0, array $args = []) /* Save arguments. */ $this->_args = $args; - /* Setup injector. */ + /* Setup injector. Only register the default bindings on an injector we + * created ourselves: registering them again on one handed over by a + * bootstrap shadows instances it already resolved (e.g. SessionLifecycle). */ + $injectorProvided = isset($GLOBALS['injector']); $injector = $GLOBALS['injector'] ??= new Horde_Injector(new Horde_Injector_TopLevel()); - (new \Horde\Core\DefaultInjectorBindings())->register($injector); + if (!$injectorProvided) { + (new \Horde\Core\DefaultInjectorBindings())->register($injector); + } $GLOBALS['registry'] = $this; $injector->setInstance(__CLASS__, $this); diff --git a/src/DefaultInjectorBindings.php b/src/DefaultInjectorBindings.php index 7774ad11..932abcb6 100644 --- a/src/DefaultInjectorBindings.php +++ b/src/DefaultInjectorBindings.php @@ -226,6 +226,7 @@ public function register(Injector $injector): void 'Horde_Perms' => 'Horde_Core_Factory_Perms', 'Horde_Perms_Base' => 'Horde_Core_Factory_Perms', 'Horde_Queue_Storage' => 'Horde_Core_Factory_QueueStorage', + 'Horde_Registry' => [Factory\HordeRegistryFactory::class, 'create'], 'Horde_Routes_Mapper' => 'Horde_Core_Factory_Mapper', HordeRoutesMapper::class => 'Horde_Core_Factory_Mapper', 'Horde_Routes_Matcher' => 'Horde_Core_Factory_Matcher', diff --git a/src/Factory/HordeRegistryFactory.php b/src/Factory/HordeRegistryFactory.php new file mode 100644 index 00000000..3001aa48 --- /dev/null +++ b/src/Factory/HordeRegistryFactory.php @@ -0,0 +1,30 @@ +