"Fossies" - the Fresh Open Source Software Archive 
Member "jitsi-meet-7305/react/features/base/util/uri.ts" (26 May 2023, 18282 Bytes) of package /linux/misc/jitsi-meet-7305.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
See also the last
Fossies "Diffs" side-by-side code changes report for "uri.ts":
jitsi-meet_8319_vs_jitsi-meet_8615.
1 import { parseURLParams } from './parseURLParams';
2 import { normalizeNFKC } from './strings';
3
4 /**
5 * Http status codes.
6 */
7 export enum StatusCode {
8 PaymentRequired = 402
9 }
10
11 /**
12 * The app linking scheme.
13 * TODO: This should be read from the manifest files later.
14 */
15 export const APP_LINK_SCHEME = 'org.jitsi.meet:';
16
17 /**
18 * A list of characters to be excluded/removed from the room component/segment
19 * of a conference/meeting URI/URL. The list is based on RFC 3986 and the jxmpp
20 * library utilized by jicofo.
21 */
22 const _ROOM_EXCLUDE_PATTERN = '[\\:\\?#\\[\\]@!$&\'()*+,;=></"]';
23
24 /**
25 * The {@link RegExp} pattern of the authority of a URI.
26 *
27 * @private
28 * @type {string}
29 */
30 const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)';
31
32 /**
33 * The {@link RegExp} pattern of the path of a URI.
34 *
35 * @private
36 * @type {string}
37 */
38 const _URI_PATH_PATTERN = '([^?#]*)';
39
40 /**
41 * The {@link RegExp} pattern of the protocol of a URI.
42 *
43 * FIXME: The URL class exposed by JavaScript will not include the colon in
44 * the protocol field. Also in other places (at the time of this writing:
45 * the DeepLinkingMobilePage.js) the APP_LINK_SCHEME does not include
46 * the double dots, so things are inconsistent.
47 *
48 * @type {string}
49 */
50 export const URI_PROTOCOL_PATTERN = '^([a-z][a-z0-9\\.\\+-]*:)';
51
52 /**
53 * Excludes/removes certain characters from a specific path part which are
54 * incompatible with Jitsi Meet on the client and/or server sides. The main
55 * use case for this method is to clean up the room name and the tenant.
56 *
57 * @param {?string} pathPart - The path part to fix.
58 * @private
59 * @returns {?string}
60 */
61 function _fixPathPart(pathPart?: string) {
62 return pathPart
63 ? pathPart.replace(new RegExp(_ROOM_EXCLUDE_PATTERN, 'g'), '')
64 : pathPart;
65 }
66
67 /**
68 * Fixes the scheme part of a specific URI (string) so that it contains a
69 * well-known scheme such as HTTP(S). For example, the mobile app implements an
70 * app-specific URI scheme in addition to Universal Links. The app-specific
71 * scheme may precede or replace the well-known scheme. In such a case, dealing
72 * with the app-specific scheme only complicates the logic and it is simpler to
73 * get rid of it (by translating the app-specific scheme into a well-known
74 * scheme).
75 *
76 * @param {string} uri - The URI (string) to fix the scheme of.
77 * @private
78 * @returns {string}
79 */
80 function _fixURIStringScheme(uri: string) {
81 const regex = new RegExp(`${URI_PROTOCOL_PATTERN}+`, 'gi');
82 const match: Array<string> | null = regex.exec(uri);
83
84 if (match) {
85 // As an implementation convenience, pick up the last scheme and make
86 // sure that it is a well-known one.
87 let protocol = match[match.length - 1].toLowerCase();
88
89 if (protocol !== 'http:' && protocol !== 'https:') {
90 protocol = 'https:';
91 }
92
93 /* eslint-disable no-param-reassign */
94
95 uri = uri.substring(regex.lastIndex);
96 if (uri.startsWith('//')) {
97 // The specified URL was not a room name only, it contained an
98 // authority.
99 uri = protocol + uri;
100 }
101
102 /* eslint-enable no-param-reassign */
103 }
104
105 return uri;
106 }
107
108 /**
109 * Converts a path to a backend-safe format, by splitting the path '/' processing each part.
110 * Properly lowercased and url encoded.
111 *
112 * @param {string?} path - The path to convert.
113 * @returns {string?}
114 */
115 export function getBackendSafePath(path?: string): string | undefined {
116 if (!path) {
117 return path;
118 }
119
120 return path
121 .split('/')
122 .map(getBackendSafeRoomName)
123 .join('/');
124 }
125
126 /**
127 * Converts a room name to a backend-safe format. Properly lowercased and url encoded.
128 *
129 * @param {string?} room - The room name to convert.
130 * @returns {string?}
131 */
132 export function getBackendSafeRoomName(room?: string): string | undefined {
133 if (!room) {
134 return room;
135 }
136
137 /* eslint-disable no-param-reassign */
138 try {
139 // We do not know if we get an already encoded string at this point
140 // as different platforms do it differently, but we need a decoded one
141 // for sure. However since decoding a non-encoded string is a noop, we're safe
142 // doing it here.
143 room = decodeURIComponent(room);
144 } catch (e) {
145 // This can happen though if we get an unencoded string and it contains
146 // some characters that look like an encoded entity, but it's not.
147 // But in this case we're fine going on...
148 }
149
150 // Normalize the character set.
151 room = normalizeNFKC(room);
152
153 // Only decoded and normalized strings can be lowercased properly.
154 room = room?.toLowerCase();
155
156 // But we still need to (re)encode it.
157 room = encodeURIComponent(room ?? '');
158 /* eslint-enable no-param-reassign */
159
160 // Unfortunately we still need to lowercase it, because encoding a string will
161 // add some uppercase characters, but some backend services
162 // expect it to be full lowercase. However lowercasing an encoded string
163 // doesn't change the string value.
164 return room.toLowerCase();
165 }
166
167 /**
168 * Gets the (Web application) context root defined by a specific location (URI).
169 *
170 * @param {Object} location - The location (URI) which defines the (Web
171 * application) context root.
172 * @public
173 * @returns {string} - The (Web application) context root defined by the
174 * specified {@code location} (URI).
175 */
176 export function getLocationContextRoot({ pathname }: { pathname: string; }) {
177 const contextRootEndIndex = pathname.lastIndexOf('/');
178
179 return (
180 contextRootEndIndex === -1
181 ? '/'
182 : pathname.substring(0, contextRootEndIndex + 1));
183 }
184
185 /**
186 * Constructs a new {@code Array} with URL parameter {@code String}s out of a
187 * specific {@code Object}.
188 *
189 * @param {Object} obj - The {@code Object} to turn into URL parameter
190 * {@code String}s.
191 * @returns {Array<string>} The {@code Array} with URL parameter {@code String}s
192 * constructed out of the specified {@code obj}.
193 */
194 function _objectToURLParamsArray(obj = {}) {
195 const params = [];
196
197 for (const key in obj) { // eslint-disable-line guard-for-in
198 try {
199 params.push(
200 `${key}=${encodeURIComponent(JSON.stringify(obj[key as keyof typeof obj]))}`);
201 } catch (e) {
202 console.warn(`Error encoding ${key}: ${e}`);
203 }
204 }
205
206 return params;
207 }
208
209 /**
210 * Parses a specific URI string into an object with the well-known properties of
211 * the {@link Location} and/or {@link URL} interfaces implemented by Web
212 * browsers. The parsing attempts to be in accord with IETF's RFC 3986.
213 *
214 * @param {string} str - The URI string to parse.
215 * @public
216 * @returns {{
217 * hash: string,
218 * host: (string|undefined),
219 * hostname: (string|undefined),
220 * pathname: string,
221 * port: (string|undefined),
222 * protocol: (string|undefined),
223 * search: string
224 * }}
225 */
226 export function parseStandardURIString(str: string) {
227 /* eslint-disable no-param-reassign */
228
229 const obj: { [key: string]: any; } = {
230 toString: _standardURIToString
231 };
232
233 let regex;
234 let match: Array<string> | null;
235
236 // XXX A URI string as defined by RFC 3986 does not contain any whitespace.
237 // Usually, a browser will have already encoded any whitespace. In order to
238 // avoid potential later problems related to whitespace in URI, strip any
239 // whitespace. Anyway, the Jitsi Meet app is not known to utilize unencoded
240 // whitespace so the stripping is deemed safe.
241 str = str.replace(/\s/g, '');
242
243 // protocol
244 regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
245 match = regex.exec(str);
246 if (match) {
247 obj.protocol = match[1].toLowerCase();
248 str = str.substring(regex.lastIndex);
249 }
250
251 // authority
252 regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi');
253 match = regex.exec(str);
254 if (match) {
255 let authority: string = match[1].substring(/* // */ 2);
256
257 str = str.substring(regex.lastIndex);
258
259 // userinfo
260 const userinfoEndIndex = authority.indexOf('@');
261
262 if (userinfoEndIndex !== -1) {
263 authority = authority.substring(userinfoEndIndex + 1);
264 }
265
266 obj.host = authority;
267
268 // port
269 const portBeginIndex = authority.lastIndexOf(':');
270
271 if (portBeginIndex !== -1) {
272 obj.port = authority.substring(portBeginIndex + 1);
273 authority = authority.substring(0, portBeginIndex);
274 }
275
276 // hostname
277 obj.hostname = authority;
278 }
279
280 // pathname
281 regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi');
282 match = regex.exec(str);
283
284 let pathname: string | undefined;
285
286 if (match) {
287 pathname = match[1];
288 str = str.substring(regex.lastIndex);
289 }
290 if (pathname) {
291 pathname.startsWith('/') || (pathname = `/${pathname}`);
292 } else {
293 pathname = '/';
294 }
295 obj.pathname = pathname;
296
297 // query
298 if (str.startsWith('?')) {
299 let hashBeginIndex = str.indexOf('#', 1);
300
301 if (hashBeginIndex === -1) {
302 hashBeginIndex = str.length;
303 }
304 obj.search = str.substring(0, hashBeginIndex);
305 str = str.substring(hashBeginIndex);
306 } else {
307 obj.search = ''; // Google Chrome
308 }
309
310 // fragment
311 obj.hash = str.startsWith('#') ? str : '';
312
313 /* eslint-enable no-param-reassign */
314
315 return obj;
316 }
317
318 /**
319 * Parses a specific URI which (supposedly) references a Jitsi Meet resource
320 * (location).
321 *
322 * @param {(string|undefined)} uri - The URI to parse which (supposedly)
323 * references a Jitsi Meet resource (location).
324 * @public
325 * @returns {{
326 * contextRoot: string,
327 * hash: string,
328 * host: string,
329 * hostname: string,
330 * pathname: string,
331 * port: string,
332 * protocol: string,
333 * room: (string|undefined),
334 * search: string
335 * }}
336 */
337 export function parseURIString(uri?: string): any {
338 if (typeof uri !== 'string') {
339 return undefined;
340 }
341
342 const obj = parseStandardURIString(_fixURIStringScheme(uri));
343
344 // XXX While the components/segments of pathname are URI encoded, Jitsi Meet
345 // on the client and/or server sides still don't support certain characters.
346 obj.pathname = obj.pathname.split('/').map((pathPart: any) => _fixPathPart(pathPart))
347 .join('/');
348
349 // Add the properties that are specific to a Jitsi Meet resource (location)
350 // such as contextRoot, room:
351
352 // contextRoot
353 // @ts-ignore
354 obj.contextRoot = getLocationContextRoot(obj);
355
356 // The room (name) is the last component/segment of pathname.
357 const { pathname } = obj;
358
359 const contextRootEndIndex = pathname.lastIndexOf('/');
360
361 obj.room = pathname.substring(contextRootEndIndex + 1) || undefined;
362
363 if (contextRootEndIndex > 1) {
364 // The part of the pathname from the beginning to the room name is the tenant.
365 obj.tenant = pathname.substring(1, contextRootEndIndex);
366 }
367
368 return obj;
369 }
370
371 /**
372 * Implements {@code href} and {@code toString} for the {@code Object} returned
373 * by {@link #parseStandardURIString}.
374 *
375 * @param {Object} [thiz] - An {@code Object} returned by
376 * {@code #parseStandardURIString} if any; otherwise, it is presumed that the
377 * function is invoked on such an instance.
378 * @returns {string}
379 */
380 function _standardURIToString(thiz?: Object) {
381 // @ts-ignore
382 // eslint-disable-next-line @typescript-eslint/no-invalid-this
383 const { hash, host, pathname, protocol, search } = thiz || this;
384 let str = '';
385
386 protocol && (str += protocol);
387
388 // TODO userinfo
389
390 host && (str += `//${host}`);
391 str += pathname || '/';
392 search && (str += search);
393 hash && (str += hash);
394
395 return str;
396 }
397
398 /**
399 * Sometimes we receive strings that we don't know if already percent-encoded, or not, due to the
400 * various sources we get URLs or room names. This function encapsulates the decoding in a safe way.
401 *
402 * @param {string} text - The text to decode.
403 * @returns {string}
404 */
405 export function safeDecodeURIComponent(text: string) {
406 try {
407 return decodeURIComponent(text);
408 } catch (e) {
409 // The text wasn't encoded.
410 }
411
412 return text;
413 }
414
415 /**
416 * Attempts to return a {@code String} representation of a specific
417 * {@code Object} which is supposed to represent a URL. Obviously, if a
418 * {@code String} is specified, it is returned. If a {@code URL} is specified,
419 * its {@code URL#href} is returned. Additionally, an {@code Object} similar to
420 * the one accepted by the constructor of Web's ExternalAPI is supported on both
421 * mobile/React Native and Web/React.
422 *
423 * @param {Object|string} obj - The URL to return a {@code String}
424 * representation of.
425 * @returns {string} - A {@code String} representation of the specified
426 * {@code obj} which is supposed to represent a URL.
427 */
428 export function toURLString(obj?: (Object | string)) {
429 let str;
430
431 switch (typeof obj) {
432 case 'object':
433 if (obj) {
434 if (obj instanceof URL) {
435 str = obj.href;
436 } else {
437 str = urlObjectToString(obj);
438 }
439 }
440 break;
441
442 case 'string':
443 str = String(obj);
444 break;
445 }
446
447 return str;
448 }
449
450 /**
451 * Attempts to return a {@code String} representation of a specific
452 * {@code Object} similar to the one accepted by the constructor
453 * of Web's ExternalAPI.
454 *
455 * @param {Object} o - The URL to return a {@code String} representation of.
456 * @returns {string} - A {@code String} representation of the specified
457 * {@code Object}.
458 */
459 export function urlObjectToString(o: { [key: string]: any; }): string | undefined {
460 // First normalize the given url. It come as o.url or split into o.serverURL
461 // and o.room.
462 let tmp;
463
464 if (o.serverURL && o.room) {
465 tmp = new URL(o.room, o.serverURL).toString();
466 } else if (o.room) {
467 tmp = o.room;
468 } else {
469 tmp = o.url || '';
470 }
471
472 const url = parseStandardURIString(_fixURIStringScheme(tmp));
473
474 // protocol
475 if (!url.protocol) {
476 let protocol: string | undefined = o.protocol || o.scheme;
477
478 if (protocol) {
479 // Protocol is supposed to be the scheme and the final ':'. Anyway,
480 // do not make a fuss if the final ':' is not there.
481 protocol.endsWith(':') || (protocol += ':');
482 url.protocol = protocol;
483 }
484 }
485
486 // authority & pathname
487 let { pathname } = url;
488
489 if (!url.host) {
490 // Web's ExternalAPI domain
491 //
492 // It may be host/hostname and pathname with the latter denoting the
493 // tenant.
494 const domain: string | undefined = o.domain || o.host || o.hostname;
495
496 if (domain) {
497 const { host, hostname, pathname: contextRoot, port }
498 = parseStandardURIString(
499
500 // XXX The value of domain in supposed to be host/hostname
501 // and, optionally, pathname. Make sure it is not taken for
502 // a pathname only.
503 _fixURIStringScheme(`${APP_LINK_SCHEME}//${domain}`));
504
505 // authority
506 if (host) {
507 url.host = host;
508 url.hostname = hostname;
509 url.port = port;
510 }
511
512 // pathname
513 pathname === '/' && contextRoot !== '/' && (pathname = contextRoot);
514 }
515 }
516
517 // pathname
518
519 // Web's ExternalAPI roomName
520 const room = o.roomName || o.room;
521
522 if (room
523 && (url.pathname.endsWith('/')
524 || !url.pathname.endsWith(`/${room}`))) {
525 pathname.endsWith('/') || (pathname += '/');
526 pathname += room;
527 }
528
529 url.pathname = pathname;
530
531 // query/search
532
533 // Web's ExternalAPI jwt and lang
534 const { jwt, lang, release } = o;
535
536 const search = new URLSearchParams(url.search);
537
538 if (jwt) {
539 search.set('jwt', jwt);
540 }
541
542 const { defaultLanguage } = o.configOverwrite || {};
543
544 if (lang || defaultLanguage) {
545 search.set('lang', lang || defaultLanguage);
546 }
547
548 if (release) {
549 search.set('release', release);
550 }
551
552 const searchString = search.toString();
553
554 if (searchString) {
555 url.search = `?${searchString}`;
556 }
557
558 // fragment/hash
559
560 let { hash } = url;
561
562 for (const urlPrefix of [ 'config', 'interfaceConfig', 'devices', 'userInfo', 'appData' ]) {
563 const urlParamsArray
564 = _objectToURLParamsArray(
565 o[`${urlPrefix}Overwrite`]
566 || o[urlPrefix]
567 || o[`${urlPrefix}Override`]);
568
569 if (urlParamsArray.length) {
570 let urlParamsString
571 = `${urlPrefix}.${urlParamsArray.join(`&${urlPrefix}.`)}`;
572
573 if (hash.length) {
574 urlParamsString = `&${urlParamsString}`;
575 } else {
576 hash = '#';
577 }
578 hash += urlParamsString;
579 }
580 }
581
582 url.hash = hash;
583
584 return url.toString() || undefined;
585 }
586
587 /**
588 * Adds hash params to URL.
589 *
590 * @param {URL} url - The URL.
591 * @param {Object} hashParamsToAdd - A map with the parameters to be set.
592 * @returns {URL} - The new URL.
593 */
594 export function addHashParamsToURL(url: URL, hashParamsToAdd: Object = {}) {
595 const params = parseURLParams(url);
596 const urlParamsArray = _objectToURLParamsArray({
597 ...params,
598 ...hashParamsToAdd
599 });
600
601 if (urlParamsArray.length) {
602 url.hash = `#${urlParamsArray.join('&')}`;
603 }
604
605 return url;
606 }
607
608 /**
609 * Returns the decoded URI.
610 *
611 * @param {string} uri - The URI to decode.
612 * @returns {string}
613 */
614 export function getDecodedURI(uri: string) {
615 return decodeURI(uri.replace(/^https?:\/\//i, ''));
616 }
617
618 /**
619 * Adds new param to a url string. Checks whether to use '?' or '&' as a separator (checks for already existing params).
620 *
621 * @param {string} url - The url to modify.
622 * @param {string} name - The param name to add.
623 * @param {string} value - The value for the param.
624 *
625 * @returns {string} - The modified url.
626 */
627 export function appendURLParam(url: string, name: string, value: string) {
628 const newUrl = new URL(url);
629
630 newUrl.searchParams.append(name, value);
631
632 return newUrl.toString();
633 }