diff --git a/src/utils/url.test.ts b/src/utils/url.test.ts new file mode 100644 index 000000000..78491902c --- /dev/null +++ b/src/utils/url.test.ts @@ -0,0 +1,78 @@ +import {getLinkProps, isAbsoluteUrl, isLinkExternal} from './url'; + +describe('URL utils check', () => { + test.each([ + ['https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash', true], + ['http://example.net/path', true], + ['/p/a/t/h?query=string&query2=1#hash', false], + ['/path', false], + ['path', false], + ])("isAbsoluteUrl('%s') should return '%s'", (url, result) => { + expect(isAbsoluteUrl(url)).toEqual(result); + }); + + test.each([ + [ + 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash', + 'example.net', + true, + ], + [ + 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash', + 'sub.example.com', + false, + ], + [ + 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash', + undefined, + true, + ], + ['http://example.net/path', 'example.net', false], + ['http://example.net/path', 'sub.example.com', true], + ['http://example.net/path', undefined, true], + ['/p/a/t/h?query=string&query2=1#hash', 'example.net', false], + ['/p/a/t/h?query=string&query2=1#hash', undefined, false], + ['/path', 'example.net', false], + ['/path', undefined, false], + ['path', 'example.net', false], + ['path', undefined, false], + ])("isLinkExternal('%s', '%s') should return '%s'", (url, hostname, result) => { + expect(isLinkExternal(url, hostname)).toEqual(result); + }); + + test.each([ + ['http://example.net/path', 'example.net', '_blank', {target: '_blank'}], + ['http://example.net/path', 'example.net', undefined, {}], + [ + 'http://example.net/path', + 'example.com', + '_blank', + {target: '_blank', rel: 'noopener noreferrer'}, + ], + [ + 'http://example.net/path', + 'example.com', + undefined, + {target: '_blank', rel: 'noopener noreferrer'}, + ], + [ + 'http://example.net/path', + undefined, + '_blank', + {target: '_blank', rel: 'noopener noreferrer'}, + ], + [ + 'http://example.net/path', + undefined, + undefined, + {target: '_blank', rel: 'noopener noreferrer'}, + ], + + ['/path', 'example.net', '_blank', {target: '_blank'}], + ['/path', 'example.net', undefined, {}], + ['/path', undefined, '_blank', {target: '_blank'}], + ['/path', undefined, undefined, {}], + ])("getLinkProps('%s', '%s', '%s') should return '%s'", (url, hostname, target, result) => { + expect(getLinkProps(url, hostname, target)).toEqual(result); + }); +}); diff --git a/src/utils/url.ts b/src/utils/url.ts index 2ebe69719..a2a16e262 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -2,30 +2,34 @@ import {parse, format} from 'url'; export type Query = Record; +const EXAMPLE_URL = 'https://example.org'; export const EXTERNAL_LINK_PROPS = {target: '_blank', rel: 'noopener noreferrer'}; export function getLinkProps(url: string, hostname?: string, target?: string) { let linkProps = {target}; - if (target === '_blank' || isLinkExternal(url, hostname)) { + if (isLinkExternal(url, hostname)) { linkProps = {...linkProps, ...EXTERNAL_LINK_PROPS}; } return linkProps; } -export function isLinkExternal(url: string, routerHostname?: string) { - if (!routerHostname) { - return true; - } - - const {hostname} = parse(url); +export function isAbsoluteUrl(url: string | URL) { + // Using example URL as base for relative links + const urlObj = new URL(url, EXAMPLE_URL); - if (!hostname) { - return false; - } + return ( + // Compare url origin with example and check that original url was not example one + urlObj.origin !== EXAMPLE_URL || (typeof url === 'string' && url.startsWith(EXAMPLE_URL)) + ); +} - return getNonLocaleHostName(hostname) !== getNonLocaleHostName(routerHostname); +export function isLinkExternal(url: string, routerHostname?: string) { + return ( + isAbsoluteUrl(url) && + getNonLocaleHostName(new URL(url).hostname) !== getNonLocaleHostName(routerHostname ?? '') + ); } export function getNonLocaleHostName(hostname: string) {