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
25 changes: 25 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,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,
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
12 changes: 7 additions & 5 deletions resources/js/components/link-topic-commits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<InertiaLinkProps['href']> };
more?: {
count: number;
visit: Pick<InertiaLinkProps, 'href' | 'method' | 'data' | 'prefetch' | 'preserveState'>;
};
}) {
return (
<div
Expand Down Expand Up @@ -283,11 +286,11 @@ function LinkTopicCommit({

function MoreCommitsLink({
count,
href,
visit,
graphParent,
}: {
count: number;
href: NonNullable<InertiaLinkProps['href']>;
visit: Pick<InertiaLinkProps, 'href' | 'method' | 'data' | 'prefetch' | 'preserveState'>;
graphParent: string;
}) {
const dotRef = useGraphNode({
Expand All @@ -300,8 +303,7 @@ function MoreCommitsLink({

return (
<Link
prefetch
href={href}
{...visit}
className="-ml-2 flex w-fit max-w-full min-w-0 items-center gap-2 rounded-md px-2 py-0.5 text-[13px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground sm:gap-2.5"
>
<span
Expand Down
38 changes: 26 additions & 12 deletions resources/js/pages/links/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,21 @@ function displayedLink(url: string): string {
return url.replace(/^https?:\/\/(?:www\.)?/u, '');
}

function linkPage(url: string) {
return LinkController.index({ query: { url } });
/** Keep navigation URLs short enough for the browser, CDN and web server. */
function linkVisit(url: string) {
const href = LinkController.index({
query: url === '' ? {} : { url },
}).url;

return href.length <= 1_800
? { href, method: 'get' as const, prefetch: true }
: {
href: LinkController.lookup().url,
method: 'post' as const,
data: { url },
prefetch: false,
preserveState: false,
};
}

export default function Index(props: Props) {
Expand Down Expand Up @@ -187,8 +200,12 @@ function TopLinks({ links }: { links: TopLink[] }) {

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 @@ -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;
Expand Down Expand Up @@ -292,8 +309,7 @@ function LinkTopic({ link }: { link: TopLink }) {
graph={{ id: nodeId, endId }}
node={
<Link
prefetch
href={href}
{...visit}
tabIndex={-1}
className="flex rounded-lg"
>
Expand All @@ -311,8 +327,7 @@ function LinkTopic({ link }: { link: TopLink }) {

<div className="min-w-0 pt-3 pr-4">
<Link
prefetch
href={href}
{...visit}
title={topic.title}
className="block truncate text-[15px] font-semibold hover:underline"
>
Expand Down Expand Up @@ -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}
/>
</div>
</section>
Expand All @@ -372,8 +387,7 @@ function QuietLinkTopic({ link }: { link: TopLink }) {
style={{ '--lane': linkLane(link.url) } as CSSProperties}
>
<Link
prefetch
href={linkPage(link.url)}
{...linkVisit(link.url)}
className="grid grid-cols-[4rem_minmax(0,1fr)] items-center py-2.5 pr-4 transition-colors hover:bg-accent focus-visible:bg-accent sm:grid-cols-[4.5rem_minmax(0,1fr)]"
>
<span className="flex justify-center">
Expand Down
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
24 changes: 24 additions & 0 deletions tests/Feature/Controllers/LinkControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});