Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / helpers / url.ts
bloba2e44c87db0061ad455016e66d558d2e0c5833da
1 import { stripLeadingSlash, stripTrailingSlash } from '@proton/shared/lib/helpers/string';
3 import type { APP_NAMES} from '../constants';
4 import { VPN_HOSTNAME } from '../constants';
5 import { APPS, APPS_CONFIGURATION, DOH_DOMAINS, LINK_TYPES } from '../constants';
6 import window from '../window';
8 const PREFIX_TO_TYPE: { [prefix: string]: LINK_TYPES | undefined } = {
9     'tel:': LINK_TYPES.PHONE,
10     'mailto:': LINK_TYPES.EMAIL,
11     'http://': LINK_TYPES.WEB,
12     'https://': LINK_TYPES.WEB,
15 const SUPPORTED_PROTOCOLS = [
16     /* extension protocols */
17     'chrome-extension:',
18     'moz-extension:',
19     /* bundled electron apps */
20     'file:',
23 const TYPE_TO_PREFIX = {
24     [LINK_TYPES.PHONE]: { regex: /^tel:/, defaultPrefix: 'tel:' },
25     [LINK_TYPES.EMAIL]: { regex: /^mailto:/, defaultPrefix: 'mailto:' },
26     [LINK_TYPES.WEB]: { regex: /^http(|s):\/\//, defaultPrefix: 'https://' },
29 export type ParsedSearchParams = Partial<Record<string, string>>;
31 // Create one big regexp of all the regexes in TYPE_TO_PREFIX.
32 // It can be used for finding a particular type from a link.
33 const ALL_REGEXP_SOURCES = (Object.keys(TYPE_TO_PREFIX) as LINK_TYPES[])
34     .map((key) => `(${TYPE_TO_PREFIX[key].regex.source})`)
35     .join('|');
37 const ALL_REGEXP = new RegExp(ALL_REGEXP_SOURCES);
39 /**
40  * Extract host
41  * @param url
42  * @returns host
43  */
44 export const getHost = (url = '') => {
45     const { host = '' } = new URL(url);
46     return host;
49 /**
50  * Extract hostname
51  * @param url
52  * @returns hostname
53  */
54 export const getHostname = (url = '') => {
55     const { hostname = '' } = new URL(url);
56     return hostname;
59 /**
60  * Converts search parameters from hash to a URLSearchParams compatible string
61  */
62 const getSearchFromHash = (search: string) => {
63     let searchHash = search;
64     if (searchHash) {
65         searchHash = searchHash[0] === '#' ? `?${search.slice(1)}` : searchHash;
66     }
67     return searchHash;
70 export const stringifySearchParams = (
71     params: { [key: string]: string | string[] | undefined },
72     prefix?: string | undefined
73 ) => {
74     const urlSearchParams = new URLSearchParams();
76     Object.entries(params)
77         .filter(([, value]) => value !== undefined && value !== '')
78         .forEach(([key, value]) => {
79             /*
80              * typescript is not able to determine that stringifiedValue
81              * can't be undefined because of the previous filter condition
82              * therefore, typecast to string
83              */
84             const stringifiedValue = Array.isArray(value) ? value.join(',') : (value as string);
86             urlSearchParams.set(key, stringifiedValue);
87         });
89     const urlSearch = urlSearchParams.toString();
91     return urlSearch !== '' && prefix !== undefined ? prefix + urlSearch : urlSearch;
94 /**
95  * Return a param (native) map based on the search string
96  */
97 export const getSearchParams = (search: string): ParsedSearchParams => {
98     const params = new URLSearchParams(getSearchFromHash(search));
100     const result: ParsedSearchParams = {};
102     params.forEach((value, key) => {
103         result[key] = value;
104     });
106     return result;
110  * Return a new pathname with the query string updated from
111  * the search input and updated with the newParams
112  */
113 export const changeSearchParams = (pathname: string, search: string, newParams: ParsedSearchParams = {}) => {
114     const params = new URLSearchParams(getSearchFromHash(search));
116     Object.keys(newParams).forEach((key) => {
117         if (newParams[key] === undefined) {
118             params.delete(key);
119         } else {
120             params.set(key, newParams[key] as string);
121         }
122     });
124     // Remove potential mailto query from the params, otherwise search will be concatenated to the mailto query
125     if (params.get('mailto')) {
126         params.delete('mailto');
127     }
129     const queryString = params.toString();
130     const urlFragment = (queryString === '' ? '' : '#') + queryString;
132     return pathname + urlFragment;
136  * Convert from a link prefix to link type.
137  */
138 const prefixToType = (prefix = 'http://') => {
139     return PREFIX_TO_TYPE[prefix];
143  * Get a link prefix from a url.
144  */
145 const getLinkPrefix = (input = ''): string | undefined => {
146     const matches = ALL_REGEXP.exec(input) || [];
147     return matches[0];
151  * Get a link type from a link.
152  */
153 export const linkToType = (link = '') => {
154     const prefix = getLinkPrefix(link);
155     return prefixToType(prefix);
159  * Strip the link prefix from a url.
160  * Leave the prefix if it's http to let the user be able to set http or https.
161  */
162 export const stripLinkPrefix = (input = '') => {
163     const prefix = getLinkPrefix(input);
164     if (!prefix || prefix.indexOf('http') !== -1) {
165         return input;
166     }
167     return input.replace(prefix, '');
171  * Try to add link prefix if missing
172  */
173 export const addLinkPrefix = (input = '', type: LINK_TYPES) => {
174     const prefix = getLinkPrefix(input);
176     if (prefix) {
177         return input;
178     }
180     const { defaultPrefix } = TYPE_TO_PREFIX[type] || {};
182     if (defaultPrefix) {
183         return `${defaultPrefix}${input}`;
184     }
186     return input;
189 // Note: This function makes some heavy assumptions on the hostname. Only intended to work on proton-domains.
190 export const getSecondLevelDomain = (hostname: string) => {
191     return hostname.slice(hostname.indexOf('.') + 1);
194 export const getRelativeApiHostname = (hostname: string) => {
195     const idx = hostname.indexOf('.');
196     const first = hostname.slice(0, idx);
197     const second = hostname.slice(idx + 1);
198     return `${first}-api.${second}`;
201 export const getIsDohDomain = (origin: string) => {
202     return DOH_DOMAINS.some((dohDomain) => origin.endsWith(dohDomain));
205 const doesHostnameLookLikeIP = (hostname: string) => {
206     // Quick helper function to tells us if hostname string seems to be IP address or DNS name.
207     // Relies on a fact, that no TLD ever will probably end with a digit. So if last char is
208     // a digit, it's probably an IP.
209     // IPv6 addresses can end with a letter, so there's additional colon check also.
210     // Probably no need ever to use slow & complicated IP regexes here, but feel free to change
211     // whenever we have such util functions available.
212     // Note: only works on hostnames (no port), not origins (can include port and protocol).
213     return /\d$/.test(hostname) || hostname.includes(':');
216 export const getApiSubdomainUrl = (pathname: string, origin: string) => {
217     const url = new URL('', origin);
219     const usePathPrefix =
220         url.hostname === 'localhost' || getIsDohDomain(url.origin) || doesHostnameLookLikeIP(url.hostname);
221     if (usePathPrefix) {
222         url.pathname = `/api${pathname}`;
223         return url;
224     }
226     url.hostname = getRelativeApiHostname(url.hostname);
227     url.pathname = pathname;
228     return url;
231 export const getAppUrlFromApiUrl = (apiUrl: string, appName: APP_NAMES) => {
232     const { subdomain } = APPS_CONFIGURATION[appName];
233     const url = new URL(apiUrl);
234     const { hostname } = url;
235     const index = hostname.indexOf('.');
236     const tail = hostname.slice(index + 1);
237     url.pathname = '';
238     url.hostname = `${subdomain}.${tail}`;
239     return url;
242 export const getAppUrlRelativeToOrigin = (origin: string, appName: APP_NAMES) => {
243     const { subdomain } = APPS_CONFIGURATION[appName];
244     const url = new URL(origin);
245     const segments = url.host.split('.');
246     segments[0] = subdomain;
247     url.hostname = segments.join('.');
248     return url;
251 let cache = '';
252 export const getStaticURL = (path: string, location = window.location) => {
253     if (
254         location.hostname === 'localhost' ||
255         getIsDohDomain(location.origin) ||
256         SUPPORTED_PROTOCOLS.includes(location.protocol)
257     ) {
258         return `https://proton.me${path}`;
259     }
261     // We create a relative URL to support the TOR domain
262     cache = cache || getSecondLevelDomain(location.hostname);
263     // The VPN domain has a different static site and the proton.me urls are not supported there
264     const hostname = cache === 'protonvpn.com' || cache === 'protonmail.com' ? 'proton.me' : cache;
265     return `https://${hostname}${path}`;
268 export const getBlogURL = (path: string) => {
269     return getStaticURL(`/blog${path}`);
272 export const getKnowledgeBaseUrl = (path: string) => {
273     return getStaticURL(`/support${path}`);
276 export const getDomainsSupportURL = () => {
277     return getStaticURL('/support/mail/custom-email-domain');
280 export const getBridgeURL = () => {
281     return getStaticURL('/mail/bridge');
284 export const getEasySwitchURL = () => {
285     return getStaticURL('/easyswitch');
288 export const getImportExportAppUrl = () => {
289     return getStaticURL('/support/proton-mail-export-tool');
292 export const getShopURL = () => {
293     return `https://shop.proton.me`;
296 export const getDownloadUrl = (path: string) => {
297     return `https://proton.me/download${path}`;
300 export const getSupportContactURL = (params: { [key: string]: string | string[] | undefined }) => {
301     return getStaticURL(`/support/contact${stringifySearchParams(params, '?')}`);
304 export const getAppStaticUrl = (app: APP_NAMES) => {
305     if (app === 'proton-mail') {
306         return getStaticURL('/mail');
307     }
309     if (app === 'proton-drive') {
310         return getStaticURL('/drive');
311     }
313     if (app === 'proton-calendar') {
314         return getStaticURL('/calendar');
315     }
317     if (app === 'proton-pass') {
318         return getStaticURL('/pass');
319     }
321     if (app === 'proton-vpn-settings') {
322         return getStaticURL('/vpn');
323     }
325     if (app === 'proton-wallet') {
326         return getStaticURL('/wallet');
327     }
329     return getStaticURL('');
332 export const getPrivacyPolicyURL = (app?: APP_NAMES) => {
333     if (app === APPS.PROTONVPN_SETTINGS) {
334         return 'https://protonvpn.com/privacy-policy';
335     }
336     return getStaticURL('/legal/privacy');
339 export const getTermsURL = (app: APP_NAMES, locale?: string) => {
340     if (app === APPS.PROTONWALLET) {
341         return getStaticURL('/legal/wallet/terms');
342     }
343     const link = locale && locale !== 'en' ? `/${locale}/legal/terms` : '/legal/terms';
344     return getStaticURL(link);
347 export const getBlackFriday2023URL = (app?: APP_NAMES) => {
348     if (app === APPS.PROTONVPN_SETTINGS) {
349         return 'https://protonvpn.com/support/black-friday-2023';
350     }
351     return getKnowledgeBaseUrl('/black-friday-2023');
354 export const getAbuseURL = () => {
355     return getStaticURL('/support/abuse');
358 export const isValidHttpUrl = (string: string) => {
359     let url;
361     try {
362         url = new URL(string);
363     } catch (_) {
364         return false;
365     }
367     return url.protocol === 'http:' || url.protocol === 'https:';
370 export const isAppFromURL = (url: string | undefined, app: APP_NAMES) => {
371     if (url) {
372         const { host } = new URL(url);
373         const segments = host.split('.');
374         return segments[0] === APPS_CONFIGURATION[app].subdomain;
375     }
376     return false;
379 const defaultUrlParameters = { load: 'ajax' };
381 export const formatURLForAjaxRequest = (
382     href: string,
383     urlParameters: Record<string, string> = defaultUrlParameters
384 ): URL => {
385     // Create a new URL object from the href
386     const url = new URL(href);
388     // Merge existing search parameters with additional urlParameters
389     const searchParams = new URLSearchParams(url.search);
390     Object.entries(urlParameters).forEach(([key, value]) => {
391         searchParams.set(key, value);
392     });
394     // Set the merged search parameters back to the URL
395     url.search = searchParams.toString();
397     // Remove the hash fragment
398     url.hash = '';
400     return url;
403 export const getPathFromLocation = (location: { pathname: string; hash: string; search: string }) => {
404     return [location.pathname, location.search, location.hash].join('');
407 export const joinPaths = (...paths: string[]) => {
408     return paths.reduce((acc, path) => {
409         return `${stripTrailingSlash(acc)}/${stripLeadingSlash(path)}`;
410     }, '');
413 export const getVpnAccountUrl = (location = window.location) => {
414     if (location.hostname === VPN_HOSTNAME) {
415         return `https://${VPN_HOSTNAME}`;
416     }
417     const secondLevelDomain = getSecondLevelDomain(location.hostname);
418     return `https://vpn.${secondLevelDomain}`;