1 import type { ReactNode, RefObject } from 'react';
2 import { useEffect, useState } from 'react';
4 import { c } from 'ttag';
6 import useModalState from '@proton/components/components/modalTwo/useModalState';
7 import useNotifications from '@proton/components/hooks/useNotifications';
8 import { PROTON_DOMAINS } from '@proton/shared/lib/constants';
9 import { getSecondLevelDomain, isSubDomain } from '@proton/shared/lib/helpers/url';
10 import type { MailSettings } from '@proton/shared/lib/interfaces';
11 import { CONFIRM_LINK } from '@proton/shared/lib/mail/mailSettings';
12 import isTruthy from '@proton/utils/isTruthy';
14 import LinkConfirmationModal from '../components/notifications/LinkConfirmationModal/LinkConfirmationModal';
15 import { getHostname, isExternal, punycodeUrl } from '../helpers/url';
16 import useHandler from './useHandler';
18 // Reference : Angular/src/app/utils/directives/linkHandler.js
20 interface UseLinkHandlerOptions {
21 onMailTo?: (src: string) => void;
22 startListening?: boolean;
24 isPhishingAttempt?: boolean;
26 type UseLinkHandler = (
27 wrapperRef: RefObject<HTMLDivElement | undefined>,
28 mailSettings?: MailSettings,
29 options?: UseLinkHandlerOptions
30 ) => { modal: ReactNode };
32 const defaultOptions: UseLinkHandlerOptions = {
36 const getSrc = (target: Element) => {
37 const extract = () => {
39 return { encoded: target.toString() || '', raw: target.getAttribute('href') || '' };
43 <a href="http://xn--rotonmail-4sg.com" rel="noreferrer nofollow noopener">Protonmail.com</a>
44 will crash --> Unspecified error. ¯\_(ツ)_/¯
45 Don't worry, target.href/getAttribute will crash too ¯\_(ツ)_/¯
47 const attr = Array.from(target.attributes).find((attr) => (attr || {}).name === 'href');
48 return { raw: attr?.nodeValue || '' };
52 // Because even the fallback can crash on IE11/Edge
60 export const useLinkHandler: UseLinkHandler = (
63 { onMailTo, startListening, isOutside, isPhishingAttempt } = defaultOptions
65 const { createNotification } = useNotifications();
66 const [link, setLink] = useState<string>();
67 const [linkConfirmationModalProps, setLinkConfirmationModalOpen, renderLinkConfirmationModal] = useModalState();
69 // eslint-disable-next-line @typescript-eslint/no-misused-promises
70 const handleClick = useHandler(async (event: Event) => {
71 const originalTarget = event.target as Element;
72 const target = originalTarget.closest('a') || originalTarget.closest('area');
78 const src = getSrc(target);
83 .t`This message may contain some links URL that cannot be properly opened by your current browser.`,
88 // IE11 and Edge random env bug... (╯°□°)╯︵ ┻━┻
90 event.preventDefault();
94 // We only handle anchor that begins with `mailto:`
95 if (src.raw.toLowerCase().startsWith('mailto:') && onMailTo) {
96 event.preventDefault();
97 event.stopPropagation(); // Required for Safari
100 * Open the composer with the given mailto address
101 * position isAfter true as the user can choose to set a body
106 const askForConfirmation = mailSettings?.ConfirmLink ?? CONFIRM_LINK.CONFIRM;
107 const hostname = getHostname(src.raw);
108 const currentDomain = getSecondLevelDomain(window.location.hostname);
111 * If the modal is already active --- do nothing
112 * ex: click on a link, open the modal, inside the continue button is an anchor with the same link.
114 if (linkConfirmationModalProps.open) {
119 * If dealing with anchors, we need to treat them separately because we use URLs with # for searching elements
121 if (src.raw.startsWith('#')) {
122 const id = src.raw.replace('#', '');
123 if (wrapperRef.current) {
124 const elementInMail = wrapperRef.current.querySelector(`[name="${id}"], [id="${id}"]`);
126 elementInMail.scrollIntoView({ behavior: 'smooth', block: 'start' });
129 event.preventDefault();
130 event.stopPropagation();
135 (askForConfirmation || isPhishingAttempt) &&
136 isExternal(src.raw, window.location.hostname) &&
137 ![...PROTON_DOMAINS, currentDomain]
138 .filter(isTruthy) // currentDomain can be null
139 .some((domain) => isSubDomain(hostname, domain))
141 event.preventDefault();
142 event.stopPropagation(); // Required for Safari
144 const link = punycodeUrl(src.encoded || src.raw);
146 setLinkConfirmationModalOpen(true);
151 if (startListening === false) {
155 // eslint-disable-next-line @typescript-eslint/no-misused-promises
156 wrapperRef.current?.addEventListener('click', handleClick, false);
158 // eslint-disable-next-line @typescript-eslint/no-misused-promises
159 wrapperRef.current?.removeEventListener('click', handleClick, false);
161 }, [startListening, wrapperRef.current]);
163 const modal = renderLinkConfirmationModal ? (
164 <LinkConfirmationModal
166 isOutside={isOutside}
167 isPhishingAttempt={isPhishingAttempt}
168 modalProps={linkConfirmationModalProps}