Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

uri.ts 18KB

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