2026-09-19 22:57:07 -04:00
|
|
|
import React, { MouseEvent, ReactNode, useState } from 'react';
|
|
|
|
|
import FocusTrap from 'focus-trap-react';
|
|
|
|
|
import {
|
|
|
|
|
Box,
|
|
|
|
|
Button,
|
|
|
|
|
Dialog,
|
|
|
|
|
Header,
|
|
|
|
|
Icon,
|
|
|
|
|
IconButton,
|
|
|
|
|
Icons,
|
|
|
|
|
Overlay,
|
|
|
|
|
OverlayBackdrop,
|
|
|
|
|
OverlayCenter,
|
|
|
|
|
Text,
|
|
|
|
|
color,
|
|
|
|
|
config,
|
|
|
|
|
} from 'folds';
|
|
|
|
|
import { stopPropagation } from '../../utils/keyboard';
|
|
|
|
|
import { LinkSafety } from '../../utils/linkSafety';
|
|
|
|
|
|
|
|
|
|
type SuspiciousLinkProps = {
|
|
|
|
|
href: string;
|
|
|
|
|
safety: LinkSafety;
|
|
|
|
|
anchorProps: Record<string, unknown>;
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* [Gitea #122] A link whose visible text names a different site than its
|
|
|
|
|
* destination (or whose destination is a punycode host) opens a small
|
|
|
|
|
* confirm instead of navigating straight away. Honest links never see this.
|
|
|
|
|
*/
|
|
|
|
|
export function SuspiciousLink({ href, safety, anchorProps, children }: SuspiciousLinkProps) {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleClick = (evt: MouseEvent) => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.stopPropagation();
|
|
|
|
|
setOpen(true);
|
|
|
|
|
};
|
|
|
|
|
const proceed = () => {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
window.open(href, '_blank', 'noopener,noreferrer');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<a
|
|
|
|
|
{...anchorProps}
|
|
|
|
|
href={href}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
data-lotus-suspicious-link
|
|
|
|
|
title={`Goes to ${safety.realHost}`}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</a>
|
|
|
|
|
{open && (
|
|
|
|
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
|
|
|
|
<OverlayCenter>
|
|
|
|
|
<FocusTrap
|
|
|
|
|
focusTrapOptions={{
|
2026-09-24 13:06:10 -04:00
|
|
|
fallbackFocus: '#suspiciouslink-dialog-1',
|
2026-09-19 22:57:07 -04:00
|
|
|
onDeactivate: () => setOpen(false),
|
|
|
|
|
clickOutsideDeactivates: true,
|
|
|
|
|
escapeDeactivates: stopPropagation,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-09-24 13:06:10 -04:00
|
|
|
<Dialog
|
|
|
|
|
id="suspiciouslink-dialog-1"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby="suspiciouslink-dialog-1-title"
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
variant="Surface"
|
|
|
|
|
style={{ maxWidth: 440 }}
|
|
|
|
|
>
|
2026-09-19 22:57:07 -04:00
|
|
|
<Header
|
|
|
|
|
style={{
|
|
|
|
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
|
|
|
|
borderBottomWidth: config.borderWidth.B300,
|
|
|
|
|
}}
|
|
|
|
|
variant="Surface"
|
|
|
|
|
size="500"
|
|
|
|
|
>
|
|
|
|
|
<Box grow="Yes">
|
2026-09-24 13:06:10 -04:00
|
|
|
<Text id="suspiciouslink-dialog-1-title" as="h2" size="H4">
|
2026-09-19 22:57:07 -04:00
|
|
|
This link doesn't go where it says
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="300"
|
|
|
|
|
onClick={() => setOpen(false)}
|
|
|
|
|
radii="300"
|
|
|
|
|
aria-label="Close"
|
|
|
|
|
>
|
|
|
|
|
<Icon src={Icons.Cross} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Header>
|
|
|
|
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
|
|
|
|
<Text>
|
|
|
|
|
{safety.shownHost ? (
|
|
|
|
|
<>
|
|
|
|
|
It shows <b>{safety.shownHost}</b> but goes to{' '}
|
|
|
|
|
<b style={{ color: color.Critical.Main }}>{safety.realHost}</b>.
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
It goes to <b style={{ color: color.Critical.Main }}>{safety.realHost}</b>.
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{safety.punycode &&
|
|
|
|
|
' The destination uses look-alike (internationalised) characters in its name.'}
|
|
|
|
|
</Text>
|
|
|
|
|
<Text size="T200" priority="300" style={{ wordBreak: 'break-all' }}>
|
|
|
|
|
{href}
|
|
|
|
|
</Text>
|
|
|
|
|
<Box gap="200" justifyContent="End">
|
|
|
|
|
<Button variant="Secondary" fill="Soft" onClick={() => setOpen(false)}>
|
|
|
|
|
<Text size="B400">Cancel</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="Critical" onClick={proceed}>
|
|
|
|
|
<Text size="B400">Open anyway</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</FocusTrap>
|
|
|
|
|
</OverlayCenter>
|
|
|
|
|
</Overlay>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|