Skip to content

Commit 1089f61

Browse files
authored
fix(http): properly handle cookies lifecycle (#1416)
1 parent 4386578 commit 1089f61

18 files changed

+76
-58
lines changed

packages/http/src/Cookie/CookieManager.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
use Tempest\Clock\Clock;
88
use Tempest\Core\AppConfig;
99
use Tempest\DateTime\DateTimeInterface;
10+
use Tempest\Support\Str;
1011

11-
use function Tempest\Support\str;
12-
12+
/**
13+
* Manages cookies that will be sent to the client.
14+
*/
1315
final class CookieManager
1416
{
1517
/** @var \Tempest\Http\Cookie\Cookie[] */
@@ -31,12 +33,15 @@ public function get(string $key): ?Cookie
3133
return $this->cookies[$key] ?? null;
3234
}
3335

36+
/**
37+
* Adds or updates a cookie that will be sent to the client in this request.
38+
*/
3439
public function set(string $key, string $value, DateTimeInterface|int|null $expiresAt = null): Cookie
3540
{
3641
$cookie = $this->get($key) ?? new Cookie(
3742
key: $key,
38-
secure: str($this->appConfig->baseUri)->startsWith('https'),
3943
path: '/',
44+
secure: Str\starts_with($this->appConfig->baseUri, 'https'),
4045
httpOnly: true,
4146
sameSite: SameSite::LAX,
4247
);
@@ -49,6 +54,9 @@ public function set(string $key, string $value, DateTimeInterface|int|null $expi
4954
return $cookie;
5055
}
5156

57+
/**
58+
* Adds a cookie that will be sent to the client in this request.
59+
*/
5260
public function add(Cookie $cookie): void
5361
{
5462
if ($cookie->expiresAt !== null) {

packages/http/src/Cookie/CookieManagerInitializer.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,9 @@
1515
#[Singleton]
1616
public function initialize(Container $container): CookieManager
1717
{
18-
$cookieManager = new CookieManager(
18+
return new CookieManager(
1919
appConfig: $container->get(AppConfig::class),
2020
clock: $container->get(Clock::class),
2121
);
22-
23-
foreach ($_COOKIE as $key => $value) {
24-
$cookieManager->add(new Cookie($key, $value));
25-
}
26-
27-
return $cookieManager;
2822
}
2923
}

packages/http/src/Input/StdinInputStream.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function parse(): array
2323
->mapWithKeys(function (string $item) {
2424
$parts = explode('=', $item, 2);
2525

26-
$key = $parts[0];
26+
$key = urldecode($parts[0]);
2727

2828
$value = $_POST[str_replace('.', '_', $key)] ?? $parts[1] ?? '';
2929

packages/http/src/IsRequest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@ trait IsRequest
4242
private(set) array $files;
4343

4444
#[SkipValidation]
45-
public array $cookies {
46-
get => get(CookieManager::class)->all();
45+
public Session $session {
46+
get => get(Session::class);
4747
}
4848

49+
#[SkipValidation]
50+
public array $cookies = [];
51+
4952
public function __construct(
5053
Method $method,
5154
string $uri,
@@ -88,10 +91,7 @@ public function getSessionValue(string $name): mixed
8891

8992
public function getCookie(string $name): ?Cookie
9093
{
91-
/** @var CookieManager $cookies */
92-
$cookies = get(CookieManager::class);
93-
94-
return $cookies->get($name);
94+
return $this->cookies[$name] ?? null;
9595
}
9696

9797
private function resolvePath(): string

packages/http/src/Mappers/PsrRequestToGenericRequestMapper.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
88
use Psr\Http\Message\UploadedFileInterface;
9+
use Tempest\Http\Cookie\Cookie;
910
use Tempest\Http\GenericRequest;
1011
use Tempest\Http\Method;
1112
use Tempest\Http\RequestHeaders;
1213
use Tempest\Http\Upload;
1314
use Tempest\Mapper\Mapper;
15+
use Tempest\Support\Arr;
1416

1517
use function Tempest\map;
1618
use function Tempest\Support\arr;
@@ -53,6 +55,7 @@ public function map(mixed $from, mixed $to): GenericRequest
5355
'path' => $from->getUri()->getPath(),
5456
'query' => $query,
5557
'files' => $uploads,
58+
'cookies' => Arr\map_iterable($_COOKIE, static fn (string $value, string $key) => new Cookie($key, $value)),
5659
...$data,
5760
...$uploads,
5861
])

packages/http/src/Session/Config/FileSessionConfig.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ public function __construct(
1717
public string $path,
1818

1919
public Duration $expiration,
20-
21-
public string $sessionIdResolver = CookieSessionIdResolver::class,
2220
) {}
2321

2422
public function createManager(Container $container): FileSessionManager

packages/http/src/Session/Resolvers/CookieSessionIdResolver.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tempest\Http\Cookie\Cookie;
1111
use Tempest\Http\Cookie\CookieManager;
1212
use Tempest\Http\Cookie\SameSite;
13+
use Tempest\Http\Request;
1314
use Tempest\Http\Session\SessionConfig;
1415
use Tempest\Http\Session\SessionId;
1516
use Tempest\Http\Session\SessionIdResolver;
@@ -20,6 +21,7 @@
2021
{
2122
public function __construct(
2223
private AppConfig $appConfig,
24+
private Request $request,
2325
private CookieManager $cookies,
2426
private SessionConfig $sessionConfig,
2527
private Clock $clock,
@@ -32,7 +34,7 @@ public function resolve(): SessionId
3234
->append('_session_id')
3335
->toString();
3436

35-
$id = $this->cookies->get($sessionKey)->value ?? null;
37+
$id = $this->request->getCookie($sessionKey)?->value;
3638

3739
if (! $id) {
3840
$id = (string) Uuid::v4();

packages/http/src/Session/Session.php

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@
1111

1212
final class Session
1313
{
14-
public const string VALIDATION_ERRORS = 'validation_errors';
14+
public const string VALIDATION_ERRORS = '#validation_errors';
1515

16-
public const string ORIGINAL_VALUES = 'original_values';
16+
public const string ORIGINAL_VALUES = '#original_values';
1717

18-
public const string PREVIOUS_URL = '_previous_url';
18+
public const string PREVIOUS_URL = '#previous_url';
1919

20-
public const string CSRF_TOKEN_KEY = '_csrf_token';
20+
public const string CSRF_TOKEN_KEY = '#csrf_token';
2121

2222
private array $expiredKeys = [];
2323

24+
private SessionManager $manager {
25+
get => get(SessionManager::class);
26+
}
27+
2428
/**
2529
* Session token used for cross-site request forgery protection.
2630
*/
@@ -43,17 +47,23 @@ public function __construct(
4347

4448
public function set(string $key, mixed $value): void
4549
{
46-
$this->getSessionManager()->set($this->id, $key, $value);
50+
$this->manager->set($this->id, $key, $value);
4751
}
4852

53+
/**
54+
* Stores a value in the session that will be available for the next request only.
55+
*/
4956
public function flash(string $key, mixed $value): void
5057
{
51-
$this->getSessionManager()->set($this->id, $key, new FlashValue($value));
58+
$this->manager->set($this->id, $key, new FlashValue($value));
5259
}
5360

61+
/**
62+
* Reflashes all flash values in the session, making them available for the next request.
63+
*/
5464
public function reflash(): void
5565
{
56-
foreach ($this->getSessionManager()->all($this->id) as $key => $value) {
66+
foreach ($this->manager->all($this->id) as $key => $value) {
5767
if (! ($value instanceof FlashValue))
5868
continue;
5969

@@ -63,7 +73,7 @@ public function reflash(): void
6373

6474
public function get(string $key, mixed $default = null): mixed
6575
{
66-
$value = $this->getSessionManager()->get($this->id, $key, $default);
76+
$value = $this->manager->get($this->id, $key, $default);
6777

6878
if ($value instanceof FlashValue) {
6979
$this->expiredKeys[$key] = $key;
@@ -75,14 +85,17 @@ public function get(string $key, mixed $default = null): mixed
7585

7686
public function getPreviousUrl(): string
7787
{
78-
return $this->get(self::PREVIOUS_URL, '');
88+
return $this->get(self::PREVIOUS_URL, default: '');
7989
}
8090

8191
public function setPreviousUrl(string $url): void
8292
{
8393
$this->set(self::PREVIOUS_URL, $url);
8494
}
8595

96+
/**
97+
* Retrieves the value for the given key and removes it from the session.
98+
*/
8699
public function consume(string $key, mixed $default = null): mixed
87100
{
88101
$value = $this->get($key, $default);
@@ -94,33 +107,28 @@ public function consume(string $key, mixed $default = null): mixed
94107

95108
public function all(): array
96109
{
97-
return $this->getSessionManager()->all($this->id);
110+
return $this->manager->all($this->id);
98111
}
99112

100113
public function remove(string $key): void
101114
{
102-
$this->getSessionManager()->remove($this->id, $key);
115+
$this->manager->remove($this->id, $key);
103116
}
104117

105118
public function destroy(): void
106119
{
107-
$this->getSessionManager()->destroy($this->id);
120+
$this->manager->destroy($this->id);
108121
}
109122

110123
public function isValid(): bool
111124
{
112-
return $this->getSessionManager()->isValid($this->id);
125+
return $this->manager->isValid($this->id);
113126
}
114127

115128
public function cleanup(): void
116129
{
117130
foreach ($this->expiredKeys as $key) {
118-
$this->getSessionManager()->remove($this->id, $key);
131+
$this->manager->remove($this->id, $key);
119132
}
120133
}
121-
122-
private function getSessionManager(): SessionManager
123-
{
124-
return get(SessionManager::class);
125-
}
126134
}

packages/http/src/Session/SessionConfig.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,5 @@ interface SessionConfig
1515
get;
1616
}
1717

18-
/**
19-
* Class responsible for resolving the session identifier.
20-
*
21-
* @var class-string<SessionIdResolver>
22-
*/
23-
public string $sessionIdResolver {
24-
get;
25-
}
26-
2718
public function createManager(Container $container): SessionManager;
2819
}

packages/http/src/Session/SessionIdResolverInitializer.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@
44

55
namespace Tempest\Http\Session;
66

7+
use Tempest\Clock\Clock;
78
use Tempest\Container\Container;
89
use Tempest\Container\Initializer;
10+
use Tempest\Core\AppConfig;
11+
use Tempest\Http\Cookie\CookieManager;
12+
use Tempest\Http\Request;
13+
use Tempest\Http\Session\Resolvers\CookieSessionIdResolver;
914
use Tempest\Http\Session\SessionConfig;
1015

1116
final readonly class SessionIdResolverInitializer implements Initializer
1217
{
1318
public function initialize(Container $container): SessionIdResolver
1419
{
15-
$config = $container->get(SessionConfig::class);
16-
17-
return $container->get($config->sessionIdResolver);
20+
return new CookieSessionIdResolver(
21+
appConfig: $container->get(AppConfig::class),
22+
request: $container->get(Request::class),
23+
cookies: $container->get(CookieManager::class),
24+
sessionConfig: $container->get(SessionConfig::class),
25+
clock: $container->get(Clock::class),
26+
);
1827
}
1928
}

0 commit comments

Comments
 (0)