Skip to content

Commit 6a6fc88

Browse files
committed
feat: add back previous url tracking
1 parent f6cce2d commit 6a6fc88

File tree

6 files changed

+420
-39
lines changed

6 files changed

+420
-39
lines changed

packages/http/src/Responses/Back.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Tempest\Http\IsResponse;
88
use Tempest\Http\Request;
99
use Tempest\Http\Response;
10-
use Tempest\Http\Session\Session;
10+
use Tempest\Http\Session\PreviousUrl;
1111
use Tempest\Http\Status;
1212

1313
use function Tempest\get;
@@ -22,20 +22,14 @@ final class Back implements Response
2222
public function __construct(?string $fallback = null)
2323
{
2424
$this->status = Status::FOUND;
25-
$request = get(Request::class);
26-
27-
$url = $request->headers['referer'] ?? $request->getSessionValue(Session::PREVIOUS_URL);
2825

29-
if ($url) {
30-
$this->addHeader('Location', $url);
31-
return;
32-
}
26+
$tracker = get(PreviousUrl::class);
27+
$request = get(Request::class);
3328

34-
if ($fallback) {
35-
$this->addHeader('Location', $fallback);
36-
return;
37-
}
29+
$url = $tracker->get(
30+
default: $request->headers['referer'] ?? $fallback ?? '/',
31+
);
3832

39-
$this->addHeader('Location', '/');
33+
$this->addHeader('Location', value: $url);
4034
}
4135
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session;
6+
7+
use Tempest\Http\Method;
8+
use Tempest\Http\Request;
9+
10+
/**
11+
* Tracks the previous URL visited by the user.
12+
*/
13+
final readonly class PreviousUrl
14+
{
15+
private const string PREVIOUS_URL_SESSION_KEY = '#previous_url';
16+
private const string INTENDED_URL_SESSION_KEY = '#intended_url';
17+
18+
public function __construct(
19+
private Session $session,
20+
) {}
21+
22+
/**
23+
* Stores the current request URL as the previous URL.
24+
*/
25+
public function track(Request $request): void
26+
{
27+
if ($this->shouldNotTrack($request)) {
28+
return;
29+
}
30+
31+
$this->session->set(self::PREVIOUS_URL_SESSION_KEY, $request->uri);
32+
}
33+
34+
/**
35+
* Gets the previous URL, or a default fallback.
36+
*/
37+
public function get(string $default = '/'): string
38+
{
39+
return $this->session->get(self::PREVIOUS_URL_SESSION_KEY, $default);
40+
}
41+
42+
/**
43+
* Stores the URL where user was trying to go before being redirected. After authentication, the user should be redirect to that URL.
44+
*/
45+
public function setIntended(string $url): void
46+
{
47+
$this->session->set(self::INTENDED_URL_SESSION_KEY, $url);
48+
}
49+
50+
/**
51+
* Gets and consume the intended URL.
52+
*/
53+
public function getIntended(string $default = '/'): string
54+
{
55+
return $this->session->consume(self::INTENDED_URL_SESSION_KEY, $default);
56+
}
57+
58+
private function shouldNotTrack(Request $request): bool
59+
{
60+
if ($request->headers->get('x-requested-with') === 'XMLHttpRequest') {
61+
return true;
62+
}
63+
64+
if ($request->method !== Method::GET) {
65+
return true;
66+
}
67+
68+
if ($request->headers->get('purpose') === 'prefetch') {
69+
return true;
70+
}
71+
72+
return false;
73+
}
74+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session;
6+
7+
use Tempest\Http\Request;
8+
use Tempest\Http\Response;
9+
use Tempest\Router\HttpMiddleware;
10+
use Tempest\Router\HttpMiddlewareCallable;
11+
12+
final readonly class TrackPreviousUrlMiddleware implements HttpMiddleware
13+
{
14+
public function __construct(
15+
private PreviousUrl $previousUrlTracker,
16+
) {}
17+
18+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
19+
{
20+
$this->previousUrlTracker->track($request);
21+
22+
return $next($request);
23+
}
24+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Http;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use Tempest\Http\GenericRequest;
9+
use Tempest\Http\Method;
10+
use Tempest\Http\Session\PreviousUrl;
11+
use Tempest\Http\Session\Session;
12+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
13+
14+
/**
15+
* @internal
16+
*/
17+
final class PreviousUrlTest extends FrameworkIntegrationTestCase
18+
{
19+
private PreviousUrl $tracker {
20+
get => $this->container->get(PreviousUrl::class);
21+
}
22+
23+
private Session $session {
24+
get => $this->container->get(Session::class);
25+
}
26+
27+
#[Test]
28+
public function tracks_get_requests(): void
29+
{
30+
$this->tracker->track(new GenericRequest(
31+
method: Method::GET,
32+
uri: '/dashboard',
33+
));
34+
35+
$this->assertEquals('/dashboard', $this->tracker->get());
36+
}
37+
38+
#[Test]
39+
public function does_not_track_post_requests(): void
40+
{
41+
$this->tracker->track(new GenericRequest(
42+
method: Method::POST,
43+
uri: '/submit-form',
44+
));
45+
46+
$this->assertEquals('/', $this->tracker->get());
47+
}
48+
49+
#[Test]
50+
public function does_not_track_ajax_requests(): void
51+
{
52+
$this->tracker->track(new GenericRequest(
53+
method: Method::GET,
54+
uri: '/api/data',
55+
headers: ['X-Requested-With' => 'XMLHttpRequest'],
56+
));
57+
58+
$this->assertEquals('/', $this->tracker->get());
59+
}
60+
61+
#[Test]
62+
public function does_not_track_prefetch_requests(): void
63+
{
64+
$this->tracker->track(new GenericRequest(
65+
method: Method::GET,
66+
uri: '/prefetch-page',
67+
headers: ['Purpose' => 'prefetch'],
68+
));
69+
70+
$this->assertEquals('/', $this->tracker->get());
71+
}
72+
73+
#[Test]
74+
public function get_returns_default_when_no_previous_url(): void
75+
{
76+
$this->assertEquals('/', $this->tracker->get());
77+
$this->assertEquals('/home', $this->tracker->get('/home'));
78+
}
79+
80+
#[Test]
81+
public function updates_previous_url_on_subsequent_tracks(): void
82+
{
83+
$this->tracker->track(new GenericRequest(method: Method::GET, uri: '/page1'));
84+
$this->assertEquals('/page1', $this->tracker->get());
85+
86+
$this->tracker->track(new GenericRequest(method: Method::GET, uri: '/page2'));
87+
$this->assertEquals('/page2', $this->tracker->get());
88+
89+
$this->tracker->track(new GenericRequest(method: Method::GET, uri: '/page3'));
90+
$this->assertEquals('/page3', $this->tracker->get());
91+
}
92+
93+
#[Test]
94+
public function set_intended_stores_url(): void
95+
{
96+
$this->tracker->setIntended('/protected-page');
97+
98+
$this->assertEquals('/protected-page', $this->session->get('#intended_url'));
99+
}
100+
101+
#[Test]
102+
public function get_intended_returns_and_removes_url(): void
103+
{
104+
$this->tracker->setIntended('/admin/dashboard');
105+
106+
$this->assertEquals('/admin/dashboard', $this->tracker->getIntended());
107+
$this->assertEquals('/', $this->tracker->getIntended());
108+
}
109+
110+
#[Test]
111+
public function get_intended_returns_default_when_not_set(): void
112+
{
113+
$this->assertEquals('/', $this->tracker->getIntended());
114+
$this->assertEquals('/fallback', $this->tracker->getIntended('/fallback'));
115+
}
116+
117+
#[Test]
118+
public function tracks_urls_with_query_strings(): void
119+
{
120+
$this->tracker->track(new GenericRequest(
121+
method: Method::GET,
122+
uri: '/search?q=tempest&filter=docs',
123+
));
124+
125+
$this->assertEquals('/search?q=tempest&filter=docs', $this->tracker->get());
126+
}
127+
128+
#[Test]
129+
public function tracks_urls_with_fragments(): void
130+
{
131+
$this->tracker->track(new GenericRequest(
132+
method: Method::GET,
133+
uri: '/docs#installation',
134+
));
135+
136+
$this->assertEquals('/docs#installation', $this->tracker->get());
137+
}
138+
}

0 commit comments

Comments
 (0)