feat: warn when go to external untrusted link

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-21 21:48:25 +08:00
parent 991bff7660
commit 06be9b58f7
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
10 changed files with 137 additions and 13 deletions

View File

@ -19,6 +19,8 @@ const createDefaultSettings = (): GeneralSettings => ({
// UX
// autoHideFeedColumn: true,
groupByDate: true,
// Secure
jumpOutLinkWarn: true,
})
export const {

View File

@ -1,4 +1,5 @@
import { FeedViewType } from "@renderer/lib/enum"
import { WarnGoToExternalLink } from "@renderer/modules/entry-content/components/WarnGoToExternalLink"
import { useEntryContentContext } from "@renderer/modules/entry-content/hooks"
import { useFeedByIdSelector } from "@renderer/store/feed"
import { useMemo } from "react"
@ -46,18 +47,19 @@ export const MarkdownLink = (props: LinkProps) => {
return (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<a
<WarnGoToExternalLink
className="follow-link--underline font-semibold text-foreground no-underline"
href={populatedFullHref}
title={props.title}
target="_blank"
rel="noreferrer"
>
{props.children}
{typeof props.children === "string" && (
<i className="i-mgc-arrow-right-up-cute-re size-[0.9em] translate-y-[2px] opacity-70" />
)}
</a>
</WarnGoToExternalLink>
</TooltipTrigger>
{!!props.href && (
<TooltipPortal>

View File

@ -3,7 +3,7 @@ import { stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { useFeedHeaderTitle } from "@renderer/store/feed"
import { useEntryContentPlaceholderLogoShow } from "./atoms"
import { useEntryContentPlaceholderLogoShow } from "../atoms"
export const EntryPlaceholderLogo = () => {
const title = useFeedHeaderTitle()

View File

@ -15,7 +15,7 @@ import { useEntryReadHistory } from "@renderer/store/entry"
import { useUserById } from "@renderer/store/user"
import { Fragment } from "react"
import { usePresentUserProfileModal } from "../profile/hooks"
import { usePresentUserProfileModal } from "../../profile/hooks"
export const EntryReadHistory: Component<{ entryId: string }> = ({
entryId,

View File

@ -0,0 +1,115 @@
import { Label } from "@radix-ui/react-label"
import { PopoverPortal } from "@radix-ui/react-popover"
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
import { Button } from "@renderer/components/ui/button"
import { Checkbox } from "@renderer/components/ui/checkbox"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@renderer/components/ui/popover"
import { jotaiStore } from "@renderer/lib/jotai"
import { getStorageNS } from "@renderer/lib/ns"
import { m } from "framer-motion"
import { atomWithStorage } from "jotai/utils"
import { forwardRef, Fragment, useState } from "react"
const TrustedKey = getStorageNS("trusted-external-link")
const trustedAtom = atomWithStorage(TrustedKey, [] as string[], undefined, {
getOnInit: true,
})
const trustedDefaultLinks = new Set([
"github.com",
"gitlab.com",
"google.com",
"sspai.com",
])
const getURLDomain = (url: string) => {
if (URL.canParse(url)) {
const urlObj = new URL(url)
return urlObj.hostname
}
return null
}
export const WarnGoToExternalLink = forwardRef<
HTMLAnchorElement,
React.DetailedHTMLProps<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>
>(({ ...rest }, ref) => {
const [open, setOpen] = useState(false)
const [checked, setChecked] = useState(false)
const shouldWarn = useGeneralSettingKey("jumpOutLinkWarn")
const handleOpen: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
rest.onClick?.(e)
if (!shouldWarn) return
const { href } = rest
if (!href) return
const domain = getURLDomain(href)
if (domain &&
!trustedDefaultLinks.has(domain) &&
!jotaiStore.get(trustedAtom).includes(domain)) {
setOpen(true)
e.preventDefault()
}
}
const handleGo = () => {
open()
if (!checked) {
return
}
const { href } = rest
if (!href) return
const domain = getURLDomain(href)
if (domain && !jotaiStore.get(trustedAtom).includes(domain)) {
jotaiStore.set(trustedAtom, (prev) => [...prev, domain])
}
function open() {
if (!rest.href) return
window.open(rest.href, "_blank", "noopener,noreferrer")
}
}
return (
<Fragment>
<Popover open={open} onOpenChange={(v) => !v && setOpen(false)}>
<PopoverTrigger asChild>
<a ref={ref} {...rest} onClick={handleOpen} />
</PopoverTrigger>
<PopoverPortal>
<PopoverContent>
<p className="text-sm">
You are about to leave this site to go to an external page, do you
trust this URL and go to it?
</p>
<p className="mt-2 text-center text-sm underline">{rest.href}</p>
<div className="mt-3 flex justify-between">
<Label className="center flex">
<Checkbox checked={checked} onCheckedChange={setChecked} />
<span className="ml-2 text-[13px]">Trust this domain</span>
</Label>
<Button
variant="ghost"
buttonClassName="px-4 hover:bg-accent bg-accent/10 dark:bg-accent/20 dark:hover:bg-accent/60"
className="group gap-2"
onClick={handleGo}
>
<m.i className="i-mingcute-arrow-right-line duration-200 group-hover:translate-x-4 group-hover:text-white dark:group-hover:text-inherit" />
<span className="duration-200 group-hover:opacity-0">Go</span>
</Button>
</div>
</PopoverContent>
</PopoverPortal>
</Popover>
</Fragment>
)
})

View File

@ -7,7 +7,7 @@ import { useEntry } from "@renderer/store/entry/hooks"
import { AnimatePresence, m } from "framer-motion"
import { useEntryContentScrollToTop, useEntryTitleMeta } from "./atoms"
import { EntryReadHistory } from "./read-history"
import { EntryReadHistory } from "./components/EntryReadHistory"
export function EntryHeader({
view,

View File

@ -36,7 +36,8 @@ import { LoadingCircle } from "../../components/ui/loading"
import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily"
import { EntryTranslation } from "../entry-column/translation"
import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms"
import { EntryPlaceholderLogo } from "./entry-placeholder"
import { EntryPlaceholderLogo } from "./components/EntryPlaceholderLogo"
import { WarnGoToExternalLink } from "./components/WarnGoToExternalLink"
import { EntryHeader } from "./header"
import { EntryContentProvider } from "./provider"
@ -147,6 +148,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({
"h-0 min-w-0 grow overflow-y-auto @container",
className,
)}
scrollbarClassName="mr-1"
viewportClassName="p-5"
ref={scrollerRef}
>
@ -167,7 +169,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({
onContextMenu={stopPropagation}
className="relative m-auto min-w-0 max-w-[550px] @3xl:max-w-[70ch]"
>
<a
<WarnGoToExternalLink
href={entry.entries.url || void 0}
target="_blank"
className="-mx-6 block cursor-default rounded-lg p-6 transition-colors hover:bg-theme-item-hover focus-visible:bg-theme-item-hover focus-visible:!outline-none @sm:-mx-3 @sm:p-3"
@ -199,7 +201,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({
</span>
</div>
</div>
</a>
</WarnGoToExternalLink>
<WrappedElementProvider boundingDetection>
<TitleMetaHandler entryId={entry.entries.id} />

View File

@ -2,9 +2,7 @@ import {
setGeneralSetting,
useGeneralSettingValue,
} from "@renderer/atoms/settings/general"
import {
createSetting,
} from "@renderer/atoms/settings/helper"
import { createSetting } from "@renderer/atoms/settings/helper"
import { initPostHog } from "@renderer/initialize/posthog"
import { tipcClient } from "@renderer/lib/client"
import { clearLocalPersistStoreData } from "@renderer/store/utils/clear"
@ -78,7 +76,11 @@ export const SettingGeneral = () => {
description:
"Automatically mark single-level entries (e.g., social media posts, pictures, video views) as read when they enter the view.",
}),
{ type: "title", value: "Secure" },
defineSettingItem("jumpOutLinkWarn", {
label: "Warn when opening external links",
description: "When you open an untrusted external link, you need to make sure that you open the link.",
}),
{
type: "title",
value: "Privacy & Data",

View File

@ -159,7 +159,7 @@
appearance: none;
border-radius: 5px;
border-width: 1px;
border-color: theme(colors.theme.inactive);
border-color: theme(colors.border);
--tw-border-opacity: 0.2;
}

View File

@ -7,6 +7,7 @@ export interface GeneralSettings {
hoverMarkUnread: boolean
renderMarkUnread: boolean
groupByDate: boolean
jumpOutLinkWarn: boolean
}
export interface UISettings {