"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 }