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 */
19 /* bundled electron apps */
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})`)
37 const ALL_REGEXP = new RegExp(ALL_REGEXP_SOURCES);
44 export const getHost = (url = '') => {
45 const { host = '' } = new URL(url);
54 export const getHostname = (url = '') => {
55 const { hostname = '' } = new URL(url);
60 * Converts search parameters from hash to a URLSearchParams compatible string
62 const getSearchFromHash = (search: string) => {
63 let searchHash = search;
65 searchHash = searchHash[0] === '#' ? `?${search.slice(1)}` : searchHash;
70 export const stringifySearchParams = (
71 params: { [key: string]: string | string[] | undefined },
72 prefix?: string | undefined
74 const urlSearchParams = new URLSearchParams();
76 Object.entries(params)
77 .filter(([, value]) => value !== undefined && value !== '')
78 .forEach(([key, value]) => {
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
84 const stringifiedValue = Array.isArray(value) ? value.join(',') : (value as string);
86 urlSearchParams.set(key, stringifiedValue);
89 const urlSearch = urlSearchParams.toString();
91 return urlSearch !== '' && prefix !== undefined ? prefix + urlSearch : urlSearch;
95 * Return a param (native) map based on the search string
97 export const getSearchParams = (search: string): ParsedSearchParams => {
98 const params = new URLSearchParams(getSearchFromHash(search));
100 const result: ParsedSearchParams = {};
102 params.forEach((value, key) => {
110 * Return a new pathname with the query string updated from
111 * the search input and updated with the newParams
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) {
120 params.set(key, newParams[key] as string);
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');
129 const queryString = params.toString();
130 const urlFragment = (queryString === '' ? '' : '#') + queryString;
132 return pathname + urlFragment;
136 * Convert from a link prefix to link type.
138 const prefixToType = (prefix = 'http://') => {
139 return PREFIX_TO_TYPE[prefix];
143 * Get a link prefix from a url.
145 const getLinkPrefix = (input = ''): string | undefined => {
146 const matches = ALL_REGEXP.exec(input) || [];
151 * Get a link type from a link.
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.
162 export const stripLinkPrefix = (input = '') => {
163 const prefix = getLinkPrefix(input);
164 if (!prefix || prefix.indexOf('http') !== -1) {
167 return input.replace(prefix, '');
171 * Try to add link prefix if missing
173 export const addLinkPrefix = (input = '', type: LINK_TYPES) => {
174 const prefix = getLinkPrefix(input);
180 const { defaultPrefix } = TYPE_TO_PREFIX[type] || {};
183 return `${defaultPrefix}${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);
222 url.pathname = `/api${pathname}`;
226 url.hostname = getRelativeApiHostname(url.hostname);
227 url.pathname = pathname;
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);
238 url.hostname = `${subdomain}.${tail}`;
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('.');
252 export const getStaticURL = (path: string, location = window.location) => {
254 location.hostname === 'localhost' ||
255 getIsDohDomain(location.origin) ||
256 SUPPORTED_PROTOCOLS.includes(location.protocol)
258 return `https://proton.me${path}`;
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');
309 if (app === 'proton-drive') {
310 return getStaticURL('/drive');
313 if (app === 'proton-calendar') {
314 return getStaticURL('/calendar');
317 if (app === 'proton-pass') {
318 return getStaticURL('/pass');
321 if (app === 'proton-vpn-settings') {
322 return getStaticURL('/vpn');
325 if (app === 'proton-wallet') {
326 return getStaticURL('/wallet');
329 return getStaticURL('');
332 export const getPrivacyPolicyURL = (app?: APP_NAMES) => {
333 if (app === APPS.PROTONVPN_SETTINGS) {
334 return 'https://protonvpn.com/privacy-policy';
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');
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';
351 return getKnowledgeBaseUrl('/black-friday-2023');
354 export const getAbuseURL = () => {
355 return getStaticURL('/support/abuse');
358 export const isValidHttpUrl = (string: string) => {
362 url = new URL(string);
367 return url.protocol === 'http:' || url.protocol === 'https:';
370 export const isAppFromURL = (url: string | undefined, app: APP_NAMES) => {
372 const { host } = new URL(url);
373 const segments = host.split('.');
374 return segments[0] === APPS_CONFIGURATION[app].subdomain;
379 const defaultUrlParameters = { load: 'ajax' };
381 export const formatURLForAjaxRequest = (
383 urlParameters: Record<string, string> = defaultUrlParameters
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);
394 // Set the merged search parameters back to the URL
395 url.search = searchParams.toString();
397 // Remove the hash fragment
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)}`;
413 export const getVpnAccountUrl = (location = window.location) => {
414 if (location.hostname === VPN_HOSTNAME) {
415 return `https://${VPN_HOSTNAME}`;
417 const secondLevelDomain = getSecondLevelDomain(location.hostname);
418 return `https://vpn.${secondLevelDomain}`;