Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/Http/Controllers/LinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use App\Queries\LinkCommitsQuery;
use App\Queries\TopLinksQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
use Illuminate\Pagination\CursorPaginator;
use Inertia\Inertia;
use Inertia\Response;
Expand All @@ -20,10 +22,32 @@
*/
final readonly class LinkController
{
public function lookup(ListLinksRequest $request): RedirectResponse
{
$url = $request->link();

if ($url === null) {
return to_route('link.index');
}

$token = Str::random(24);
$lookups = $request->session()->get('link_lookups', []);
$lookups[$token] = $url;
$request->session()->put('link_lookups', array_slice($lookups, -10, null, true));

return to_route('link.index', ['lookup' => $token]);
}

public function index(ListLinksRequest $request, #[CurrentUser] User $user, LinkCommitsQuery $commits, TopLinksQuery $topLinks): Response
{
$url = $request->link();

if ($url === null && $request->filled('lookup')) {
$token = $request->string('lookup')->value();
$url = $request->session()->get('link_lookups.'.$token);
abort_unless(is_string($url), 404);
}

if ($url === null) {
return Inertia::render('links/index', [
'url' => null,
Expand Down
1 change: 1 addition & 0 deletions app/Http/Requests/ListLinksRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function rules(): array
{
return [
'url' => ['nullable', 'string', 'max:'.Urls::MAX_LENGTH],
'lookup' => ['sometimes', 'string', 'regex:/^[A-Za-z0-9]{24}$/'],
];
}

Expand Down
32 changes: 26 additions & 6 deletions resources/js/pages/links/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,35 @@ function peopleCount(count: number) {
return count === 1 ? '1 person' : `${count} people`;
}

function linkVisit(url: string) {
const destination = LinkController.index({
query: url === '' ? {} : { url },
}).url;

if (destination.length > 1_800) {
return {
href: LinkController.lookup().url,
method: 'post' as const,
data: { url },
preserveState: false,
};
}

return { href: destination, method: 'get' as const, data: {} };
}

export default function Index(props: Props) {
const [draft, setDraft] = useState(props.url ?? '');

const submit = (event: FormEvent) => {
event.preventDefault();
const url = draft.trim();
router.get(LinkController.index({ query: url === '' ? {} : { url } }));
const visit = linkVisit(draft.trim());

if (visit.method === 'post') {
router.post(visit.href, visit.data, { preserveState: false });
} else {
router.get(visit.href);
}
};

return (
Expand Down Expand Up @@ -109,10 +131,8 @@ function TopLinks({ links }: { links: TopLink[] }) {
{links.map((link, index) => (
<li key={link.url} className="border-b">
<Link
prefetch
href={LinkController.index({
query: { url: link.url },
})}
prefetch={linkVisit(link.url).method === 'get'}
{...linkVisit(link.url)}
className="flex gap-3 px-4 py-3 transition-colors hover:bg-accent focus-visible:bg-accent sm:px-6"
>
<span className="w-5 shrink-0 pt-0.5 text-right font-mono text-[13px] text-muted-foreground tabular-nums">
Expand Down
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@

// Every commit about a link, a prototype only reachable by its URL...
Route::get('links', [LinkController::class, 'index'])->name('link.index');
Route::post('links/lookup', [LinkController::class, 'lookup'])->name('link.lookup');

// Commit...
Route::get('scheduled-commits', [ScheduledCommitController::class, 'index'])->name('scheduled-commit.index');
Expand Down
30 changes: 30 additions & 0 deletions tests/Feature/Controllers/LinkControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,33 @@
->where('stats', ['commits' => 0, 'people' => 0])
->where('commits', null));
});

it('uses a short session-backed GET URL for oversized link lookups', function (): void {
$url = 'https://example.com/'.str_repeat('a', 1900);
$link = LinkPreview::factory()->fetched()->create(['url' => $url]);
$first = Commit::factory()->create(['link_preview_id' => $link->id]);

$response = $this->actingAs(User::factory()->create())
->post(route('link.lookup'), ['url' => $url])
->assertRedirect();

$location = $response->headers->get('Location');
expect($location)->toBeString()->toContain('lookup=');

// The GET destination remains valid on refresh and for cursor pagination.
$this->get($location)
->assertOk()
->assertInertia(fn ($page) => $page
->component('links/index')
->where('url', $url)
->where('commits.data.0.id', $first->id));

$this->get($location)->assertOk()->assertInertia(fn ($page) => $page
->where('url', $url));
});

it('does not accept an unknown long-link lookup token', function (): void {
$this->actingAs(User::factory()->create())
->get(route('link.index', ['lookup' => str_repeat('X', 24)]))
->assertNotFound();
});