Public: anyone can read this; the crew can change it.

The 404 page offers a search for what was asked, and a stranger Create an account beside Sign in #8582

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/8483-the-404-page-offers-a-search-for-what-wa → main 1 commit fork 2 threads · 2 unresolved

  • Merged. torque/8483-the-404-page-offers-a-search-for-what-wa was merged into main as 7454c34.

Review

Sign in to review this change.
3 files changed +45 −4
src/web/chrome.ts +19 −3
@@ -72,8 +72,12 @@ export function crumbsFor(repo: Repo): { label: string; href: string }[] {
7272 * this address; `plain` leaves that out for a miss that must reveal nothing
7373 * (a dead `/go/:code`). */
7474 export function notFoundPage(ctx: Ctx, opts: { plain?: boolean } = {}): Response {
75 const offerSignIn =
76 !opts.plain && ctx.auth.user === undefined && ctx.request.method === "GET";
75 const get = !opts.plain && ctx.request.method === "GET";
76 const offerSignIn = get && ctx.auth.user === undefined;
77 const asked = get ? askedFor(ctx.url.pathname) : "";
78 const search = asked
79 ? btn(`Search for ‘${asked}’`, { href: `/search?q=${encodeURIComponent(asked)}` })
80 : html``;
7781 return page(
7882 {
7983 ctx,
@@ -87,8 +91,9 @@ export function notFoundPage(ctx: Ctx, opts: { plain?: boolean } = {}): Response
8791 ? btn("Go home", { href: "/", variant: "primary" })

Next: when signInHref returns /signup (edge stranger), Sign in and Create an account are the same destination; show only one.

8892 : offerSignIn
8993 ? html`${btn("Sign in", { href: signInHref(ctx), variant: "primary" })}
94 ${btn("Create an account", { href: "/signup" })} ${search}
9095 ${btn("Go home", { href: "/" })} ${btn("Explore", { href: "/explore" })}`
91 : html`${btn("Go home", { href: "/", variant: "primary" })}
96 : html`${btn("Go home", { href: "/", variant: "primary" })} ${search}
9297 ${btn("Explore", { href: "/explore" })}`,
9398 })}`,
9499 },
@@ -96,6 +101,17 @@ export function notFoundPage(ctx: Ctx, opts: { plain?: boolean } = {}): Response
96101 );
97102 }
98103
104 /** What the reader asked for: the last path segment, URL-decoded, trimmed,
105 * at most 100 characters; empty for `/` or an undecodable segment. */
106 function askedFor(pathname: string): string {
107 const segment = pathname.slice(pathname.lastIndexOf("/") + 1);
108 try {

Next: a trailing slash gives an empty last segment, so no search is offered; skip empty segments.

109 return decodeURIComponent(segment).trim().slice(0, 100).trim();
110 } catch {
111 return "";
112 }
113 }
114
99115 export const repoNotFound = notFoundPage;
100116
101117 /** Why a refusal happened, in the words a person reads on the 403 page. */
test/web-origins.test.ts +3
@@ -771,6 +771,9 @@ describe("origins on the web", () => {
771771 redirect: "manual",
772772 });
773773 expect(unknown.status).toBe(404);
774 const plainBody = await unknown.clone().text();
775 expect(plainBody).not.toContain("/search?q=");
776 expect(plainBody).not.toContain("Create an account");
774777 app.db
775778 .query("UPDATE oauth_flow SET expires_at = ?1 WHERE id = ?2")
776779 .run("2020-01-01T00:00:00.000Z", flowId);
test/web-ui.test.ts +23 −1
@@ -456,7 +456,7 @@ describe("web ui", () => {
456456 const hidden = await (await fetch(`${base}/kim/secret`)).text();
457457 const absent = await (await fetch(`${base}/kim/nothing`)).text();
458458 const scrub = (t: string) =>
459 t.replaceAll(/nonce-?="?[\w-]+/g, "nonce").replaceAll(/here\?path=[^"&\s]*/g, "here?path=").replaceAll(/next=[^"]*/g, "");
459 t.replaceAll(/nonce-?="?[\w-]+/g, "nonce").replaceAll(/here\?path=[^"&\s]*/g, "here?path=").replaceAll(/next=[^"]*/g, "").replaceAll(/q=[^"]*"/g, 'q="').replaceAll(/Search for ‘[^’]*’/g, "Search for");
460460 expect(scrub(hidden)).toBe(scrub(absent));
461461
462462 const signedIn = await (
@@ -468,6 +468,28 @@ describe("web ui", () => {
468468 expect(signedIn).toContain('href="/explore"');
469469 });
470470
471 test("a 404 offers a search for what was asked, and a stranger an account", async () => {
472 const out = await (await fetch(`${base}/kim/nothing`)).text();
473 expect(out).toContain('href="/search?q=nothing"');
474 expect(out).toContain("Search for ‘nothing’");
475 expect(out).toContain('href="/signup"');
476 expect(out).toContain("Create an account");
477
478 const odd = await (await fetch(`${base}/%3Cx%3E`)).text();
479 expect(odd).toContain("Search for ‘<x>’");
480 expect(odd).toContain('href="/search?q=%3Cx%3E"');
481 expect(odd).not.toContain("<x>");
482
483 const long = await (await fetch(`${base}/${"a".repeat(150)}`)).text();
484 expect(long).toContain(`q=${"a".repeat(100)}"`);
485
486 const signedIn = await (
487 await fetch(`${base}/kim/nothing`, { headers: { cookie: stranger } })
488 ).text();
489 expect(signedIn).toContain("Search for ‘nothing’");
490 expect(signedIn).not.toContain("Create an account");
491 });
492
471493 test("safeNext refuses another origin", () => {
472494 expect(safeNext("//evil.example/x")).toBeUndefined();
473495 expect(safeNext("https://evil.example")).toBeUndefined();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs