Skip to content

Commit 3753189

Browse files
committed
refactor(SessionHandler): SessionAdministrator decorates SessionHandler instead of holding components
Eliminates the three-component construction (backend, factory, serializer) in favor of a single SessionHandler dependency. Operations route through SessionHandler's existing public API: - listAll/load/destroy/expire/getMetadata are pass-throughs - findByUser and destroyAllForUser compose over the pass-throughs Adds expire() and getMetadata() pass-throughs that were previously missing — admin callers should never need to inject both SessionAdministrator and SessionHandler side by side. DI is now trivial: the Injector autowires the SessionHandler dependency, no factory class needed.
1 parent c45b9eb commit 3753189

1 file changed

Lines changed: 68 additions & 51 deletions

File tree

src/SessionAdministrator.php

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,43 @@
1515

1616
use Generator;
1717
use Horde\SessionHandler\Exception\CapabilityException;
18-
use Horde\SessionHandler\Exception\SessionException;
1918

2019
/**
2120
* Cross-session inspection and administration service.
2221
*
23-
* Operates on stored sessions other than the one currently active in this
24-
* PHP process. Callers include admin tools (list active sessions, force
25-
* logout one), security flows (password change → invalidate all OTHER
26-
* sessions for the user), and ops jobs (background cleanup).
22+
* Decorates {@see SessionHandler} with operations relevant to admin tools
23+
* and security flows: list, load, force-destroy, look up by user, sign-out
24+
* everywhere. Callers include `base/admin/sessions.php`, password-change
25+
* handlers ("invalidate other sessions for this user"), and ops jobs.
2726
*
28-
* This is deliberately separate from {@see SessionHandler}, which manages
29-
* the lifecycle of the SINGLE active session in the current request.
27+
* Design rule: code that reaches for SessionAdministrator should never
28+
* also need to reach for {@see SessionHandler}. Every operation that an
29+
* admin caller might need is exposed here either as a pass-through to
30+
* SessionHandler (for the lifecycle ops that apply to any session by id)
31+
* or as a user-aware method composed over those primitives. SessionHandler
32+
* itself stays focused on the active-session lifecycle.
33+
*
34+
* Active-session-only operations on SessionHandler — `create()`, `save()`,
35+
* `regenerate()` and the SessionHandlerInterface plumbing — are
36+
* deliberately NOT proxied. They don't make sense for arbitrary stored
37+
* sessions; admin tools should not be able to mint new ones, mutate
38+
* others' state, or rotate IDs of foreign sessions.
3039
*
3140
* Capability requirements
3241
* -----------------------
33-
* The injected backend must implement {@see SessionStorageBackend} (mandatory)
34-
* for delete/load. Methods that enumerate sessions additionally require
35-
* {@see IterableSessionBackend}; calling them against a non-iterable backend
36-
* throws {@see CapabilityException}.
42+
* The capability checks belong to the underlying SessionHandler — we just
43+
* forward calls. listAll() throws {@see CapabilityException} when the
44+
* backend is not iterable; getMetadata() throws when the backend lacks
45+
* metadata support; expire() throws when the backend lacks administrative
46+
* expiration support. Mandatory ops (load, destroy) work on any backend.
3747
*
38-
* Methods that filter by user (findByUser, destroyAllForUser) inspect the
39-
* payload of each enumerated session. The user identifier is read via the
40-
* "horde/auth/userId" path used by HordeSession, matching the wire layout
41-
* of {@see \Horde\Core\Session\HordeSession::getAuthenticatedUser()}. Other
42-
* Session implementations can extract via the same path or override by
43-
* subclassing.
48+
* User-aware methods
49+
* ------------------
50+
* findByUser/destroyAllForUser inspect the payload of each enumerated
51+
* session via the protected {@see extractUserId()} hook. The default
52+
* implementation reads "horde/auth/userId" — the wire layout used by
53+
* {@see \Horde\Core\Session\HordeSession::getAuthenticatedUser()}. Other
54+
* Session implementations can override extractUserId() in a subclass.
4455
*
4556
* Performance note
4657
* ----------------
@@ -52,71 +63,77 @@
5263
class SessionAdministrator
5364
{
5465
public function __construct(
55-
protected readonly SessionStorageBackend $backend,
56-
protected readonly SessionFactory $factory,
57-
protected readonly SessionSerializer $serializer,
66+
protected readonly SessionHandler $handler,
5867
) {}
5968

6069
/**
61-
* Enumerate all known session IDs in the backend.
70+
* Enumerate all known session IDs.
6271
*
6372
* @return Generator<SessionId>
6473
*
6574
* @throws CapabilityException If the backend cannot enumerate sessions.
66-
* @throws SessionException On backend failure during enumeration.
6775
*/
6876
public function listAll(): Generator
6977
{
70-
if (!$this->backend instanceof IterableSessionBackend) {
71-
throw new CapabilityException(
72-
'Backend ' . $this->backend::class . ' does not support session enumeration; '
73-
. 'implement IterableSessionBackend to enable listing.'
74-
);
75-
}
76-
77-
foreach ($this->backend->listSessions() as $id) {
78+
foreach ($this->handler->listSessions() as $id) {
7879
yield $id;
7980
}
8081
}
8182

8283
/**
8384
* Load a stored session by ID.
8485
*
85-
* Returns null if the session does not exist, has expired, or its
86-
* payload is corrupt and cannot be deserialized.
86+
* Pass-through to {@see SessionHandler::load()}. Returns null if the
87+
* session does not exist, has expired, or its payload is corrupt.
8788
*/
8889
public function load(SessionId $id): ?Session
8990
{
90-
$payload = $this->backend->load($id);
91-
if ($payload === null) {
92-
return null;
93-
}
94-
95-
try {
96-
$data = $this->serializer->deserialize($payload);
97-
} catch (SessionException) {
98-
return null;
99-
}
100-
101-
return $this->factory->restore($id, $data);
91+
return $this->handler->load($id);
10292
}
10393

10494
/**
10595
* Forcibly delete a session.
10696
*
107-
* Idempotent: removing a non-existent session is not an error.
97+
* Pass-through to {@see SessionHandler::destroySession()}. Idempotent.
10898
*/
10999
public function destroy(SessionId $id): void
110100
{
111-
$this->backend->delete($id);
101+
$this->handler->destroySession($id);
102+
}
103+
104+
/**
105+
* Mark a session expired without deleting it.
106+
*
107+
* Pass-through to {@see SessionHandler::expire()}. Useful for "soft
108+
* logout" flows where a record of the session should remain for audit
109+
* but the session is no longer valid for authentication.
110+
*
111+
* @throws CapabilityException If the backend does not support
112+
* administrative expiration.
113+
*/
114+
public function expire(SessionId $id): void
115+
{
116+
$this->handler->expire($id);
117+
}
118+
119+
/**
120+
* Retrieve session metadata (creation time, last modification, expiry).
121+
*
122+
* Pass-through to {@see SessionHandler::getMetadata()}. Returns null
123+
* when the backend supports metadata but no record matches the id.
124+
*
125+
* @throws CapabilityException If the backend does not support metadata.
126+
*/
127+
public function getMetadata(SessionId $id): ?SessionMetadata
128+
{
129+
return $this->handler->getMetadata($id);
112130
}
113131

114132
/**
115133
* Locate every session whose authenticated user matches $userId.
116134
*
117-
* Iterates the backend and inspects each payload. Sessions that fail
118-
* to deserialize are silently skipped — they're invalid and should be
119-
* cleaned up by GC, not reported here.
135+
* Iterates and inspects each payload. Sessions that fail to load are
136+
* silently skipped — they're invalid, GC will clean them.
120137
*
121138
* @return list<SessionId>
122139
*
@@ -144,8 +161,8 @@ public function findByUser(string $userId): array
144161
* Forcibly destroy all of $userId's sessions, optionally keeping one.
145162
*
146163
* Used by "sign out everywhere" flows and by password-change handlers
147-
* that want to invalidate other live sessions while leaving the
148-
* current one intact.
164+
* that invalidate other live sessions while leaving the caller's
165+
* current session intact.
149166
*
150167
* @param SessionId|null $exceptId Session to keep (typically the
151168
* caller's current session ID).

0 commit comments

Comments
 (0)