import { useEffect , useState , RefObject } from 'react' ; import { c } from 'ttag' ; import punycode from 'punycode.js' ; import { getSecondLevelDomain } from '@proton/shared/lib/helpers/url' ; import { MailSettings } from '@proton/shared/lib/interfaces' ; import { isIE11 , isEdge } from '@proton/shared/lib/helpers/browser' ; import isTruthy from '@proton/shared/lib/helpers/isTruthy' ; import { PROTON_DOMAINS } from '@proton/shared/lib/constants' ; import { isExternal , isSubDomain , getHostname } from '../helpers/url' ; import { useModals , useNotifications , useMailSettings , useHandler } from './index' ; import LinkConfirmationModal from '../components/notifications/LinkConfirmationModal' ; interface LinkSource { raw : string ; encoded ? : string ; } interface UseLinkHandlerOptions { onMailTo ? : ( src : string ) => void ; startListening ? : boolean ; } type UseLinkHandler = ( wrapperRef : RefObject < HTMLDivElement | undefined > , options ? : UseLinkHandlerOptions ) => void ; const defaultOptions = { startListening : true } ; export const useLinkHandler = ( wrapperRef , { onMailTo , startListening } = defaultOptions ) => { const [ mailSettings ] = useMailSettings ( ) as [ MailSettings | undefined , boolean , Error ] ; const { createModal } = useModals ( ) ; const { createNotification } = useNotifications ( ) ; const [ confirmationModalID , setConfirmationModalID ] = useState < string > ( ) ; const getSrc = ( target ) : LinkSource => { const extract = ( ) => { try { return { encoded : target . toString ( ) || '' , raw : target . getAttribute ( 'href' ) || '' } ; } catch ( e ) { const attr = Array . from ( target . attributes ) . find ( ( attr ) => ( attr || { } ) . name === 'href' ) ; return { raw : attr ?. nodeValue || '' } ; } } ; try { return extract ( ) ; } catch ( e ) { createNotification ( { text : c ( 'Error' ) . t `This message may contain some link's URL that cannot be properly opened by your current browser` , type : 'error' , } ) ; return { raw : '' } ; } } ; param { String } options . raw param { String } options . encoded return { String } const encoder = async ( { raw = '' , encoded } ) => { const noEncoding = isIE11 ( ) || isEdge ( ) || ! /:\/\/xn--/ . test ( encoded || raw ) ; if ( noEncoding ) { const [ protocol , url = '' , ... tracking ] = raw . split ( '://' ) ; const parser = ( input ) => { const uri = ! input . startsWith ( '%' ) ? input : decodeURIComponent ( input ) ; return uri . split ( '/' ) . map ( punycode . toASCII ) . join ( '/' ) ; } ; const newUrl = [ url , ... tracking ] . map ( parser ) . join ( '://' ) ; return `${ protocol }://${ newUrl }` ; } return encoded ; } ; const handleClick = useHandler ( async ( event ) => { const originalTarget = event . target as Element ; const target = originalTarget . closest ( 'a' ) ; if ( ! target ) { return ; } const src = getSrc ( target ) ; if ( ! src ) { event . preventDefault ( ) ; return false ; } if ( src . raw . toLowerCase ( ) . startsWith ( 'mailto:' ) && onMailTo ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; onMailTo ( src . raw ) ; } const askForConfirmation = mailSettings ?. ConfirmLink === undefined ? 1 : mailSettings ?. ConfirmLink ; const hostname = getHostname ( src . raw ) ; const currentDomain = getSecondLevelDomain ( window . location . hostname ) ; if ( confirmationModalID !== undefined ) { return ; } if ( src . raw . startsWith ( '#' ) ) { const id = src . raw . replace ( '#' , '' ) ; if ( wrapperRef . current ) { const elementInMail = wrapperRef . current . querySelector ( `a[name="${ id }"], a[id="${ id }"]` ) ; if ( elementInMail ) { elementInMail . scrollIntoView ( { behavior : 'smooth' , block : 'center' } ) ; } } event . preventDefault ( ) ; event . stopPropagation ( ) ; return ; } if ( askForConfirmation && isExternal ( src . raw ) && ! [ ... PROTON_DOMAINS , currentDomain ] . filter ( isTruthy ) . some ( ( domain ) => isSubDomain ( hostname , domain ) ) ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; const link = await encoder ( src ) ; const modalId = createModal ( < LinkConfirmationModal link = { link } onClose = { ( ) => setConfirmationModalID ( undefined ) } />); setConfirmationModalID ( modalId ) ; } } ) ; useEffect ( ( ) => { if ( startListening === false ) { return ; } wrapperRef . current ?. addEventListener ( 'click' , handleClick , false ) ; return ( ) => { wrapperRef . current ?. removeEventListener ( 'click' , handleClick , false ) ; } ; } , [ startListening ] ) ; } ;