diff --git a/app/Http/Controllers/LinkController.php b/app/Http/Controllers/LinkController.php index 9da6d7842..9925ed49f 100644 --- a/app/Http/Controllers/LinkController.php +++ b/app/Http/Controllers/LinkController.php @@ -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; @@ -20,10 +22,33 @@ */ final readonly class LinkController { + /** Submit large URLs through a normal POST, then redirect to a short refreshable GET URL. */ + 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, diff --git a/app/Http/Requests/ListLinksRequest.php b/app/Http/Requests/ListLinksRequest.php index df2b2cfd6..7a9e3ad0c 100644 --- a/app/Http/Requests/ListLinksRequest.php +++ b/app/Http/Requests/ListLinksRequest.php @@ -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}$/'], ]; } diff --git a/resources/js/components/link-topic-commits.tsx b/resources/js/components/link-topic-commits.tsx index 760507855..72b2f70f6 100644 --- a/resources/js/components/link-topic-commits.tsx +++ b/resources/js/components/link-topic-commits.tsx @@ -61,7 +61,10 @@ export function LinkTopicCommits({ /** The node the commits branch off. */ graphParent: string; /** The link's other commits, when it has more than the ones listed. */ - more?: { count: number; href: NonNullable }; + more?: { + count: number; + visit: Pick; + }; }) { return (
; + visit: Pick; graphParent: string; }) { const dotRef = useGraphNode({ @@ -300,8 +303,7 @@ function MoreCommitsLink({ return ( { 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 ( @@ -258,7 +275,7 @@ function TopLinks({ links }: { links: TopLink[] }) { function LinkTopic({ link }: { link: TopLink }) { const topic = linkTopic(link.url, link); const lane = linkLane(link.url); - const href = linkPage(link.url); + const visit = linkVisit(link.url); const nodeId = `topic:${link.url}`; const endId = `${nodeId}:end`; const hasMore = link.commits > link.top_commits.length; @@ -292,8 +309,7 @@ function LinkTopic({ link }: { link: TopLink }) { graph={{ id: nodeId, endId }} node={ @@ -311,8 +327,7 @@ function LinkTopic({ link }: { link: TopLink }) {
@@ -352,7 +367,7 @@ function LinkTopic({ link }: { link: TopLink }) { commits={link.top_commits} lane={lane} graphParent={endId} - more={hasMore ? { count: link.commits, href } : undefined} + more={hasMore ? { count: link.commits, visit } : undefined} />
@@ -372,8 +387,7 @@ function QuietLinkTopic({ link }: { link: TopLink }) { style={{ '--lane': linkLane(link.url) } as CSSProperties} > diff --git a/routes/web.php b/routes/web.php index bf2b27185..711bd7717 100644 --- a/routes/web.php +++ b/routes/web.php @@ -434,6 +434,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'); diff --git a/tests/Feature/Controllers/LinkControllerTest.php b/tests/Feature/Controllers/LinkControllerTest.php index bd220f410..0a67f5b3f 100644 --- a/tests/Feature/Controllers/LinkControllerTest.php +++ b/tests/Feature/Controllers/LinkControllerTest.php @@ -179,3 +179,27 @@ ->where('stats', ['commits' => 0, 'people' => 0]) ->where('commits', null)); }); + +it('redirects an oversized link lookup to a refreshable session-backed GET URL', function (): void { + $url = 'https://example.com/'.str_repeat('a', 1900); + $link = LinkPreview::factory()->fetched()->create(['url' => $url]); + $commit = Commit::factory()->create(['link_preview_id' => $link->id]); + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post(route('link.lookup'), ['url' => $url])->assertRedirect(); + $destination = $response->headers->get('Location'); + expect($destination)->toBeString()->toContain('lookup='); + + $this->get($destination)->assertOk()->assertInertia(fn ($page) => $page + ->component('links/index') + ->where('url', $url) + ->where('commits.data.0.id', $commit->id)); + + $this->get($destination)->assertOk()->assertInertia(fn ($page) => $page->where('url', $url)); +}); + +it('rejects an unknown long-link lookup token', function (): void { + $this->actingAs(User::factory()->create()) + ->get(route('link.index', ['lookup' => str_repeat('X', 24)])) + ->assertNotFound(); +});