From 0e2df242d02818679f635f44627040902f96cde4 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Thu, 9 Apr 2026 15:50:46 +0100 Subject: [PATCH] feat: formatted links in descriptions --- src/app/out/page.tsx | 72 +++++++++++++++++++++++++++ src/components/description.tsx | 91 +++++++++++----------------------- 2 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 src/app/out/page.tsx diff --git a/src/app/out/page.tsx b/src/app/out/page.tsx new file mode 100644 index 0000000..d72c91b --- /dev/null +++ b/src/app/out/page.tsx @@ -0,0 +1,72 @@ +import { Metadata } from "next"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { Icon } from "@iconify/react"; + +export const metadata: Metadata = { + title: "Leaving TomodachiShare", + description: "Warning: You are leaving TomodachiShare, proceed with caution", +}; + +interface Props { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +export default async function LinkOutPage({ searchParams }: Props) { + const url = (await searchParams).url; + if (!url || Array.isArray(url)) redirect("/"); + + let parsed: URL; + try { + parsed = new URL(url); + } catch { + redirect("/"); // redirect if URL is invalid + } + + // Next.js doesn't allow attacks like these but you can never be too safe + if (!["http:", "https:"].includes(parsed.protocol)) redirect("/"); + + const isSafe = Array.from(SAFE_LINKS).some((domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`)); + if (isSafe) redirect(url); + + return ( +
+
+

+ + Warning +

+

You're attempting to leave TomodachiShare island! The destination website is potentially dangerous.

+ +
+ {url} +
+ +
+ + + Travel Back + + + + Continue + +
+
+
+ ); +} + +const SAFE_LINKS = new Set([ + "tomodachishare.com", + "trafficlunar.net", + "youtube.com", + "youtu.be", + "twitter.com", + "x.com", + "reddit.com", + "tiktok.com", + "tumblr.com", + "instagram.com", + "wikipedia.org", +]); diff --git a/src/components/description.tsx b/src/components/description.tsx index deacf3f..4f1349d 100644 --- a/src/components/description.tsx +++ b/src/components/description.tsx @@ -1,78 +1,43 @@ -import Image from "next/image"; +import { Icon } from "@iconify/react"; import Link from "next/link"; -import { prisma } from "@/lib/prisma"; - -import ProfilePicture from "./profile-picture"; - interface Props { text: string; className?: string; } +// Adds fancy formatting to links export default function Description({ text, className }: Props) { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const parts = text.split(urlRegex); + return (

- {/* Adds fancy formatting when linking to other pages on the site */} - {(() => { - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://tomodachishare.com"; + {parts.map(async (part, index) => { + try { + // Check if it's a URL + if (!urlRegex.test(part)) throw new Error("Not a URL"); + const url = new URL(part); - // Match both mii and profile links - const regex = new RegExp(`(${baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}/(?:mii|profile)/\\d+)`, "g"); - const parts = text.split(regex); - - return parts.map(async (part, index) => { - const miiMatch = part.match(new RegExp(`^${baseUrl}/mii/(\\d+)$`)); - const profileMatch = part.match(new RegExp(`^${baseUrl}/profile/(\\d+)$`)); - - if (miiMatch) { - const id = Number(miiMatch[1]); - const linkedMii = await prisma.mii.findUnique({ - where: { - id, - }, - }); - - if (!linkedMii) return; - - return ( - - mii - {linkedMii.name} - - ); - } - - if (profileMatch) { - const id = Number(profileMatch[1]); - const linkedProfile = await prisma.user.findUnique({ - where: { - id, - }, - }); - - if (!linkedProfile) return; - - return ( - - - {linkedProfile.name} - - ); - } - - // Regular text + return ( + + {url.hostname} + {url.pathname !== "/" ? url.pathname : ""} + {url.search} + + + ); + } catch { + // Normal text/Invalid URL fallback return {part}; - }); - })()} + } + })}

); }