feat: warn when go to external untrusted link
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
991bff7660
commit
06be9b58f7
|
|
@ -19,6 +19,8 @@ const createDefaultSettings = (): GeneralSettings => ({
|
|||
// UX
|
||||
// autoHideFeedColumn: true,
|
||||
groupByDate: true,
|
||||
// Secure
|
||||
jumpOutLinkWarn: true,
|
||||
})
|
||||
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface GeneralSettings {
|
|||
hoverMarkUnread: boolean
|
||||
renderMarkUnread: boolean
|
||||
groupByDate: boolean
|
||||
jumpOutLinkWarn: boolean
|
||||
}
|
||||
|
||||
export interface UISettings {
|
||||
|
|
|
|||
Loading…
Reference in New Issue