Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / helpers / url.ts
blob506d1d93137cbde50373177a779122541989db20
1 import { stripLeadingSlash, stripTrailingSlash } from '@proton/shared/lib/helpers/string';
3 import { type ReturnUrlContext } from '../authentication/fork';
4 import { returnUrlContextKey, returnUrlKey } from '../authentication/fork/constants';
5 import type { APP_NAMES } from '../constants';
6 import { VPN_HOSTNAME } from '../constants';
7 import { APPS, APPS_CONFIGURATION, DOH_DOMAINS, LINK_TYPES } from '../constants';
8 import window from '../window';
10 const PREFIX_TO_TYPE: { [prefix: string]: LINK_TYPES | undefined } = {
11     'tel:': LINK_TYPES.PHONE,
12     'mailto:': LINK_TYPES.EMAIL,
13     'http://': LINK_TYPES.WEB,
14     'https://': LINK_TYPES.WEB,
17 const SUPPORTED_PROTOCOLS = [
18     /* extension protocols */
19     'chrome-extension:',
20     'moz-extension:',
21     /* bundled electron apps */
22     'file:',
25 const TYPE_TO_PREFIX = {
26     [LINK_TYPES.PHONE]: { regex: /^tel:/, defaultPrefix: 'tel:' },
27     [LINK_TYPES.EMAIL]: { regex: /^mailto:/, defaultPrefix: 'mailto:' },
28     [LINK_TYPES.WEB]: { regex: /^http(|s):\/\//, defaultPrefix: 'https://' },
31 export type ParsedSearchParams = Partial<Record<string, string>>;
33 // Create one big regexp of all the regexes in TYPE_TO_PREFIX.
34 // It can be used for finding a particular type from a link.
35 const ALL_REGEXP_SOURCES = (Object.keys(TYPE_TO_PREFIX) as LINK_TYPES[])
36     .map((key) => `(${TYPE_TO_PREFIX[key].regex.source})`)
37     .join('|');
39 const ALL_REGEXP = new RegExp(ALL_REGEXP_SOURCES);
41 /**
42  * Extract host
43  * @param url
44  * @returns host
45  */
46 export const getHost = (url = '') => {
47     const { host = '' } = new URL(url);
48     return host;
51 /**
52  * Extract hostname
53  * @param url
54  * @returns hostname
55  */
56 export const getHostname = (url = '') => {
57     const { hostname = '' } = new URL(url);
58     return hostname;
61 /**
62  * Converts search parameters from hash to a URLSearchParams compatible string
63  */
64 const getSearchFromHash = (search: string) => {
65     let searchHash = search;
66     if (searchHash) {
67         searchHash = searchHash[0] === '#' ? `?${search.slice(1)}` : searchHash;
68     }
69     return searchHash;
72 export const stringifySearchParams = (
73     params: { [key: string]: string | string[] | undefined },
74     prefix?: string | undefined
75 ) => {
76     const urlSearchParams = new URLSearchParams();
78     Object.entries(params)
79         .filter(([, value]) => value !== undefined && value !== '')
80         .forEach(([key, value]) => {
81             /*
82              * typescript is not able to determine that stringifiedValue
83              * can't be undefined because of the previous filter condition
84              * therefore, typecast to string
85              */
86             const stringifiedValue = Array.isArray(value) ? value.join(',') : (value as string);
88             urlSearchParams.set(key, stringifiedValue);
89         });
91     const urlSearch = urlSearchParams.toString();
93     return urlSearch !== '' && prefix !== undefined ? prefix + urlSearch : urlSearch;
96 /**
97  * Return a param (native) map based on the search string
98  */
99 export const getSearchParams = (search: string): ParsedSearchParams => {
100     const params = new URLSearchParams(getSearchFromHash(search));
102     const result: ParsedSearchParams = {};
104     params.forEach((value, key) => {
105         result[key] = value;
106     });
108     return result;
112  * Return a new pathname with the query string updated from
113  * the search input and updated with the newParams
114  */
115 export const changeSearchParams = (pathname: string, search: string, newParams: ParsedSearchParams = {}) => {
116     const params = new URLSearchParams(getSearchFromHash(search));
118     Object.keys(newParams).forEach((key) => {
119         if (newParams[key] === undefined) {
120             params.delete(key);
121         } else {
122             params.set(key, newParams[key] as string);
123         }
124     });
126     // Remove potential mailto query from the params, otherwise search will be concatenated to the mailto query
127     if (params.get('mailto')) {
128         params.delete('mailto');
129     }
131     const queryString = params.toString();
132     const urlFragment = (queryString === '' ? '' : '#') + queryString;
134     return pathname + urlFragment;
138  * Convert from a link prefix to link type.
139  */
140 const prefixToType = (prefix = 'http://') => {
141     return PREFIX_TO_TYPE[prefix];
145  * Get a link prefix from a url.
146  */
147 const getLinkPrefix = (input = ''): string | undefined => {
148     const matches = ALL_REGEXP.exec(input) || [];
149     return matches[0];
153  * Get a link type from a link.
154  */
155 export const linkToType = (link = '') => {
156     const prefix = getLinkPrefix(link);
157     return prefixToType(prefix);
161  * Strip the link prefix from a url.
162  * Leave the prefix if it's http to let the user be able to set http or https.
163  */
164 export const stripLinkPrefix = (input = '') => {
165     const prefix = getLinkPrefix(input);
166     if (!prefix || prefix.indexOf('http') !== -1) {
167         return input;
168     }
169     return input.replace(prefix, '');
173  * Try to add link prefix if missing
174  */
175 export const addLinkPrefix = (input = '', type: LINK_TYPES) => {
176     const prefix = getLinkPrefix(input);
178     if (prefix) {
179         return input;
180     }
182     const { defaultPrefix } = TYPE_TO_PREFIX[type] || {};
184     if (defaultPrefix) {
185         return `${defaultPrefix}${input}`;
186     }
188     return input;
191 // Note: This function makes some heavy assumptions on the hostname. Only intended to work on proton-domains.
192 export const getSecondLevelDomain = (hostname: string) => {
193     return hostname.slice(hostname.indexOf('.') + 1);
196 export const getRelativeApiHostname = (hostname: string) => {
197     const idx = hostname.indexOf('.');
198     const first = hostname.slice(0, idx);
199     const second = hostname.slice(idx + 1);
200     return `${first}-api.${second}`;
203 export const getIsDohDomain = (origin: string) => {
204     return DOH_DOMAINS.some((dohDomain) => origin.endsWith(dohDomain));
207 export const isSubDomain = (hostname: string, domain: string) => {
208     if (hostname === domain) {
209         return true;
210     }
211     return hostname.endsWith(`.${domain}`);
214 export const isURLProtonInternal = (url: string, hostname: string) => {
215     const currentDomain = getSecondLevelDomain(hostname);
216     const targetOriginHostname = getHostname(url);
218     // Still need to check the current domain otherwise it would not work on proton.local, localhost, etc...
219     return [
220         currentDomain,
221         'proton.me',
222         'protonmail.com',
223         'protonmailrmez3lotccipshtkleegetolb73fuirgj7r4o4vfu7ozyd.onion',
224     ].some((domain) => isSubDomain(targetOriginHostname, domain));
227 export const getIsConvertHostname = (hostname: string) => {
228     return hostname === 'join.protonvpn.com';
231 const doesHostnameLookLikeIP = (hostname: string) => {
232     // Quick helper function to tells us if hostname string seems to be IP address or DNS name.
233     // Relies on a fact, that no TLD ever will probably end with a digit. So if last char is
234     // a digit, it's probably an IP.
235     // IPv6 addresses can end with a letter, so there's additional colon check also.
236     // Probably no need ever to use slow & complicated IP regexes here, but feel free to change
237     // whenever we have such util functions available.
238     // Note: only works on hostnames (no port), not origins (can include port and protocol).
239     return /\d$/.test(hostname) || hostname.includes(':');
242 export const getApiSubdomainUrl = (pathname: string, origin: string) => {
243     const url = new URL('', origin);
245     const usePathPrefix =
246         url.hostname === 'localhost' || getIsDohDomain(url.origin) || doesHostnameLookLikeIP(url.hostname);
247     if (usePathPrefix) {
248         url.pathname = `/api${pathname}`;
249         return url;
250     }
252     url.hostname = getRelativeApiHostname(url.hostname);
253     url.pathname = pathname;
254     return url;
257 export const getAppUrlFromApiUrl = (apiUrl: string, appName: APP_NAMES) => {
258     const { subdomain } = APPS_CONFIGURATION[appName];
259     const url = new URL(apiUrl);
260     const { hostname } = url;
261     const index = hostname.indexOf('.');
262     const tail = hostname.slice(index + 1);
263     url.pathname = '';
264     url.hostname = `${subdomain}.${tail}`;
265     return url;
268 export const getAppUrlRelativeToOrigin = (origin: string, appName: APP_NAMES) => {
269     const { subdomain } = APPS_CONFIGURATION[appName];
270     const url = new URL(origin);
271     const segments = url.host.split('.');
272     segments[0] = subdomain;
273     url.hostname = segments.join('.');
274     return url;
277 let cache = '';
278 export const getStaticURL = (path: string, location = window.location) => {
279     if (
280         location.hostname === 'localhost' ||
281         getIsDohDomain(location.origin) ||
282         SUPPORTED_PROTOCOLS.includes(location.protocol)
283     ) {
284         return `https://proton.me${path}`;
285     }
287     // We create a relative URL to support the TOR domain
288     cache = cache || getSecondLevelDomain(location.hostname);
289     // The VPN domain has a different static site and the proton.me urls are not supported there
290     const hostname = cache === 'protonvpn.com' || cache === 'protonmail.com' ? 'proton.me' : cache;
291     return `https://${hostname}${path}`;
294 export const getBlogURL = (path: string) => {
295     return getStaticURL(`/blog${path}`);
298 export const getKnowledgeBaseUrl = (path: string) => {
299     return getStaticURL(`/support${path}`);
302 export const getDomainsSupportURL = () => {
303     return getStaticURL('/support/mail/custom-email-domain');
306 export const getBridgeURL = () => {
307     return getStaticURL('/mail/bridge');
310 export const getEasySwitchURL = () => {
311     return getStaticURL('/easyswitch');
314 export const getImportExportAppUrl = () => {
315     return getStaticURL('/support/proton-mail-export-tool');
318 export const getShopURL = () => {
319     return `https://shop.proton.me`;
322 export const getDownloadUrl = (path: string) => {
323     return `https://proton.me/download${path}`;
326 export const getSupportContactURL = (params: { [key: string]: string | string[] | undefined }) => {
327     return getStaticURL(`/support/contact${stringifySearchParams(params, '?')}`);
330 export const getAppStaticUrl = (app: APP_NAMES) => {
331     if (app === 'proton-mail') {
332         return getStaticURL('/mail');
333     }
335     if (app === 'proton-drive') {
336         return getStaticURL('/drive');
337     }
339     if (app === 'proton-calendar') {
340         return getStaticURL('/calendar');
341     }
343     if (app === 'proton-pass') {
344         return getStaticURL('/pass');
345     }
347     if (app === 'proton-vpn-settings') {
348         return getStaticURL('/vpn');
349     }
351     if (app === 'proton-wallet') {
352         return getStaticURL('/wallet');
353     }
355     return getStaticURL('');
358 export const getPrivacyPolicyURL = (app?: APP_NAMES) => {
359     if (app === APPS.PROTONVPN_SETTINGS) {
360         return 'https://protonvpn.com/privacy-policy';
361     }
362     return getStaticURL('/legal/privacy');
365 export const getTermsURL = (app: APP_NAMES, locale?: string) => {
366     if (app === APPS.PROTONWALLET) {
367         return getStaticURL('/legal/wallet/terms');
368     }
369     const link = locale && locale !== 'en' ? `/${locale}/legal/terms` : '/legal/terms';
370     return getStaticURL(link);
373 export const getBlackFriday2023URL = (app?: APP_NAMES) => {
374     if (app === APPS.PROTONVPN_SETTINGS) {
375         return 'https://protonvpn.com/support/black-friday-2023';
376     }
377     return getKnowledgeBaseUrl('/black-friday-2023');
380 export const getAbuseURL = () => {
381     return getStaticURL('/support/abuse');
384 export const isValidHttpUrl = (string: string) => {
385     let url;
387     try {
388         url = new URL(string);
389     } catch (_) {
390         return false;
391     }
393     return url.protocol === 'http:' || url.protocol === 'https:';
396 export const isAppFromURL = (url: string | undefined, app: APP_NAMES) => {
397     if (url) {
398         const { host } = new URL(url);
399         const segments = host.split('.');
400         return segments[0] === APPS_CONFIGURATION[app].subdomain;
401     }
402     return false;
405 const defaultUrlParameters = { load: 'ajax' };
407 export const formatURLForAjaxRequest = (
408     href: string,
409     urlParameters: Record<string, string> = defaultUrlParameters
410 ): URL => {
411     // Create a new URL object from the href
412     const url = new URL(href);
414     // Merge existing search parameters with additional urlParameters
415     const searchParams = new URLSearchParams(url.search);
416     Object.entries(urlParameters).forEach(([key, value]) => {
417         searchParams.set(key, value);
418     });
420     // Set the merged search parameters back to the URL
421     url.search = searchParams.toString();
423     // Remove the hash fragment
424     url.hash = '';
426     return url;
429 export const getPathFromLocation = (location: { pathname: string; hash: string; search: string }) => {
430     return [location.pathname, location.search, location.hash].join('');
433 export const joinPaths = (...paths: string[]) => {
434     return paths.reduce((acc, path) => {
435         return `${stripTrailingSlash(acc)}/${stripLeadingSlash(path)}`;
436     }, '');
439 export const getVpnAccountUrl = (location = window.location) => {
440     if (location.hostname === VPN_HOSTNAME) {
441         return `https://${VPN_HOSTNAME}`;
442     }
443     const secondLevelDomain = getSecondLevelDomain(location.hostname);
444     return `https://vpn.${secondLevelDomain}`;
447 export const getUrlWithReturnUrl = (
448     url: string,
449     {
450         returnUrl = getPathFromLocation(window.location),
451         context,
452     }: { returnUrl?: string; context?: ReturnUrlContext } = {}
453 ) => {
454     const urlWithReturnUrl = new URL(url);
455     urlWithReturnUrl.searchParams.append(returnUrlKey, returnUrl);
456     if (context) {
457         urlWithReturnUrl.searchParams.append(returnUrlContextKey, context);
458     }
459     return urlWithReturnUrl.toString();
463  * Append search params to a url
465  * @param url The url you want to append the search params to
466  * @param params
467  * Object with the search params you want to append.
468  * Key is the search param name and value is the search param value
469  * @throws if the url is not a valid url
470  * @returns the url with the search params appended
472  * @description
473  * Please note this method will append search params event if they already exist in the url.
474  * So if you want to override a search param you need to remove it first.
475  */
476 export const appendUrlSearchParams = (url: string, params: Record<string, string>) => {
477     const urlObj = new URL(url);
478     Object.entries(params).forEach(([key, value]) => {
479         urlObj.searchParams.append(key, value);
480     });
481     return urlObj.toString();