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 */
21 /* bundled electron apps */
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})`)
39 const ALL_REGEXP = new RegExp(ALL_REGEXP_SOURCES);
46 export const getHost = (url = '') => {
47 const { host = '' } = new URL(url);
56 export const getHostname = (url = '') => {
57 const { hostname = '' } = new URL(url);
62 * Converts search parameters from hash to a URLSearchParams compatible string
64 const getSearchFromHash = (search: string) => {
65 let searchHash = search;
67 searchHash = searchHash[0] === '#' ? `?${search.slice(1)}` : searchHash;
72 export const stringifySearchParams = (
73 params: { [key: string]: string | string[] | undefined },
74 prefix?: string | undefined
76 const urlSearchParams = new URLSearchParams();
78 Object.entries(params)
79 .filter(([, value]) => value !== undefined && value !== '')
80 .forEach(([key, value]) => {
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
86 const stringifiedValue = Array.isArray(value) ? value.join(',') : (value as string);
88 urlSearchParams.set(key, stringifiedValue);
91 const urlSearch = urlSearchParams.toString();
93 return urlSearch !== '' && prefix !== undefined ? prefix + urlSearch : urlSearch;
97 * Return a param (native) map based on the search string
99 export const getSearchParams = (search: string): ParsedSearchParams => {
100 const params = new URLSearchParams(getSearchFromHash(search));
102 const result: ParsedSearchParams = {};
104 params.forEach((value, key) => {
112 * Return a new pathname with the query string updated from
113 * the search input and updated with the newParams
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) {
122 params.set(key, newParams[key] as string);
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');
131 const queryString = params.toString();
132 const urlFragment = (queryString === '' ? '' : '#') + queryString;
134 return pathname + urlFragment;
138 * Convert from a link prefix to link type.
140 const prefixToType = (prefix = 'http://') => {
141 return PREFIX_TO_TYPE[prefix];
145 * Get a link prefix from a url.
147 const getLinkPrefix = (input = ''): string | undefined => {
148 const matches = ALL_REGEXP.exec(input) || [];
153 * Get a link type from a link.
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.
164 export const stripLinkPrefix = (input = '') => {
165 const prefix = getLinkPrefix(input);
166 if (!prefix || prefix.indexOf('http') !== -1) {
169 return input.replace(prefix, '');
173 * Try to add link prefix if missing
175 export const addLinkPrefix = (input = '', type: LINK_TYPES) => {
176 const prefix = getLinkPrefix(input);
182 const { defaultPrefix } = TYPE_TO_PREFIX[type] || {};
185 return `${defaultPrefix}${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) {
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...
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);
248 url.pathname = `/api${pathname}`;
252 url.hostname = getRelativeApiHostname(url.hostname);
253 url.pathname = pathname;
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);
264 url.hostname = `${subdomain}.${tail}`;
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('.');
278 export const getStaticURL = (path: string, location = window.location) => {
280 location.hostname === 'localhost' ||
281 getIsDohDomain(location.origin) ||
282 SUPPORTED_PROTOCOLS.includes(location.protocol)
284 return `https://proton.me${path}`;
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');
335 if (app === 'proton-drive') {
336 return getStaticURL('/drive');
339 if (app === 'proton-calendar') {
340 return getStaticURL('/calendar');
343 if (app === 'proton-pass') {
344 return getStaticURL('/pass');
347 if (app === 'proton-vpn-settings') {
348 return getStaticURL('/vpn');
351 if (app === 'proton-wallet') {
352 return getStaticURL('/wallet');
355 return getStaticURL('');
358 export const getPrivacyPolicyURL = (app?: APP_NAMES) => {
359 if (app === APPS.PROTONVPN_SETTINGS) {
360 return 'https://protonvpn.com/privacy-policy';
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');
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';
377 return getKnowledgeBaseUrl('/black-friday-2023');
380 export const getAbuseURL = () => {
381 return getStaticURL('/support/abuse');
384 export const isValidHttpUrl = (string: string) => {
388 url = new URL(string);
393 return url.protocol === 'http:' || url.protocol === 'https:';
396 export const isAppFromURL = (url: string | undefined, app: APP_NAMES) => {
398 const { host } = new URL(url);
399 const segments = host.split('.');
400 return segments[0] === APPS_CONFIGURATION[app].subdomain;
405 const defaultUrlParameters = { load: 'ajax' };
407 export const formatURLForAjaxRequest = (
409 urlParameters: Record<string, string> = defaultUrlParameters
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);
420 // Set the merged search parameters back to the URL
421 url.search = searchParams.toString();
423 // Remove the hash fragment
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)}`;
439 export const getVpnAccountUrl = (location = window.location) => {
440 if (location.hostname === VPN_HOSTNAME) {
441 return `https://${VPN_HOSTNAME}`;
443 const secondLevelDomain = getSecondLevelDomain(location.hostname);
444 return `https://vpn.${secondLevelDomain}`;
447 export const getUrlWithReturnUrl = (
450 returnUrl = getPathFromLocation(window.location),
452 }: { returnUrl?: string; context?: ReturnUrlContext } = {}
454 const urlWithReturnUrl = new URL(url);
455 urlWithReturnUrl.searchParams.append(returnUrlKey, returnUrl);
457 urlWithReturnUrl.searchParams.append(returnUrlContextKey, context);
459 return urlWithReturnUrl.toString();
463 * Append search params to a url
465 * @param url The url you want to append the search params to
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
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.
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);
481 return urlObj.toString();