diff --git a/conference.js b/conference.js index d3d0759f15..16c7a83811 100644 --- a/conference.js +++ b/conference.js @@ -19,7 +19,6 @@ import analytics from './modules/analytics/analytics'; import EventEmitter from "events"; -import { getLocationContextRoot } from './react/features/app'; import { AVATAR_ID_COMMAND, AVATAR_URL_COMMAND, @@ -50,6 +49,7 @@ import { trackAdded, trackRemoved } from './react/features/base/tracks'; +import { getLocationContextRoot } from './react/features/base/util'; import { statsEmitter } from './react/features/connection-indicator'; import { showDesktopPicker } from './react/features/desktop-picker'; import { diff --git a/react/features/app/actions.js b/react/features/app/actions.js index 1d05b82ec4..7bf95a82cc 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -2,9 +2,9 @@ import { setRoom } from '../base/conference'; import { setLocationURL } from '../base/connection'; import { setConfig } from '../base/config'; import { loadConfig } from '../base/lib-jitsi-meet'; +import { parseURIString } from '../base/util'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; -import { _parseURIString } from './functions'; declare var APP: Object; @@ -19,9 +19,7 @@ declare var APP: Object; */ export function appNavigate(uri: ?string) { return (dispatch: Dispatch<*>, getState: Function) => - _appNavigateToOptionalLocation( - dispatch, getState, - _parseURIString(uri)); + _appNavigateToOptionalLocation(dispatch, getState, parseURIString(uri)); } /** @@ -133,7 +131,7 @@ function _appNavigateToOptionalLocation( // default. if (!location || !location.host) { const defaultLocation - = _parseURIString(getState()['features/app'].app._getDefaultURL()); + = parseURIString(getState()['features/app'].app._getDefaultURL()); if (location) { location.host = defaultLocation.host; @@ -211,9 +209,7 @@ function _loadConfig(location: Object) { // The React Native app supports an app-specific scheme which is sure to not // be supported by fetch (or whatever loadConfig utilizes). - if (protocol !== 'http:' && protocol !== 'https:') { - protocol = 'https:'; - } + protocol !== 'http:' && protocol !== 'https:' && (protocol = 'https:'); // TDOO userinfo diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index 5113ceb6ac..e62a1738e0 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -1,8 +1,8 @@ -import { AbstractApp } from './AbstractApp'; -import { getLocationContextRoot } from '../functions'; - +import { getLocationContextRoot } from '../../base/util'; import '../../room-lock'; +import { AbstractApp } from './AbstractApp'; + /** * Root application component. * diff --git a/react/features/app/functions.native.js b/react/features/app/functions.native.js index cbb4aacb8c..6d21cb66ed 100644 --- a/react/features/app/functions.native.js +++ b/react/features/app/functions.native.js @@ -3,132 +3,6 @@ import { RouteRegistry } from '../base/react'; import { Conference } from '../conference'; import { WelcomePage } from '../welcome'; -/** - * The RegExp pattern of the authority of a URI. - * - * @private - * @type {string} - */ -const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)'; - -/** - * The RegExp pattern of the path of a URI. - * - * @private - * @type {string} - */ -const _URI_PATH_PATTERN = '([^?#]*)'; - -/** - * The RegExp patther of the protocol of a URI. - * - * @private - * @type {string} - */ -const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)'; - -/** - * Fixes the hier-part of a specific URI (string) so that the URI is well-known. - * For example, certain Jitsi Meet deployments are not conventional but it is - * possible to translate their URLs into conventional. - * - * @param {string} uri - The URI (string) to fix the hier-part of. - * @private - * @returns {string} - */ -function _fixURIStringHierPart(uri) { - // Rewrite the specified URL in order to handle special cases such as - // hipchat.com and enso.me which do not follow the common pattern of most - // Jitsi Meet deployments. - - // hipchat.com - let regex - = new RegExp( - `^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`, - 'gi'); - let match = regex.exec(uri); - - if (!match) { - // enso.me - regex - = new RegExp( - `^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`, - 'gi'); - match = regex.exec(uri); - } - if (match) { - /* eslint-disable no-param-reassign, prefer-template */ - - uri - = match[1] /* protocol */ - + '//enso.hipchat.me/' - + uri.substring(regex.lastIndex); /* room (name) */ - - /* eslint-enable no-param-reassign, prefer-template */ - } - - return uri; -} - -/** - * Fixes the scheme part of a specific URI (string) so that it contains a - * well-known scheme such as HTTP(S). For example, the mobile app implements an - * app-specific URI scheme in addition to Universal Links. The app-specific - * scheme may precede or replace the well-known scheme. In such a case, dealing - * with the app-specific scheme only complicates the logic and it is simpler to - * get rid of it (by translating the app-specific scheme into a well-known - * scheme). - * - * @param {string} uri - The URI (string) to fix the scheme of. - * @private - * @returns {string} - */ -function _fixURIStringScheme(uri) { - const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi'); - const match = regex.exec(uri); - - if (match) { - // As an implementation convenience, pick up the last scheme and make - // sure that it is a well-known one. - let protocol = match[match.length - 1].toLowerCase(); - - if (protocol !== 'http:' && protocol !== 'https:') { - protocol = 'https:'; - } - - /* eslint-disable no-param-reassign */ - - uri = uri.substring(regex.lastIndex); - if (uri.startsWith('//')) { - // The specified URL was not a room name only, it contained an - // authority. - uri = protocol + uri; - } - - /* eslint-enable no-param-reassign */ - } - - return uri; -} - -/** - * Gets the (Web application) context root defined by a specific location (URI). - * - * @param {Object} location - The location (URI) which defines the (Web - * application) context root. - * @returns {string} - The (Web application) context root defined by the - * specified {@code location} (URI). - */ -export function getLocationContextRoot(location: Object) { - const pathname = location.pathname; - const contextRootEndIndex = pathname.lastIndexOf('/'); - - return ( - contextRootEndIndex === -1 - ? '/' - : pathname.substring(0, contextRootEndIndex + 1)); -} - /** * Determines which route is to be rendered in order to depict a specific Redux * store. @@ -159,137 +33,3 @@ export function _getRouteToRender(stateOrGetState) { return RouteRegistry.getRouteByComponent(component); } - -/** - * Parses a specific URI string into an object with the well-known properties of - * the {@link Location} and/or {@link URL} interfaces implemented by Web - * browsers. The parsing attempts to be in accord with IETF's RFC 3986. - * - * @param {string} str - The URI string to parse. - * @returns {{ - * hash: string, - * host: (string|undefined), - * hostname: (string|undefined), - * pathname: string, - * port: (string|undefined), - * protocol: (string|undefined), - * search: string - * }} - */ -function _parseStandardURIString(str: string) { - /* eslint-disable no-param-reassign */ - - const obj = {}; - - let regex; - let match; - - // protocol - regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi'); - match = regex.exec(str); - if (match) { - obj.protocol = match[1].toLowerCase(); - str = str.substring(regex.lastIndex); - } - - // authority - regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi'); - match = regex.exec(str); - if (match) { - let authority = match[1].substring(/* // */ 2); - - str = str.substring(regex.lastIndex); - - // userinfo - const userinfoEndIndex = authority.indexOf('@'); - - if (userinfoEndIndex !== -1) { - authority = authority.substring(userinfoEndIndex + 1); - } - - obj.host = authority; - - // port - const portBeginIndex = authority.lastIndexOf(':'); - - if (portBeginIndex !== -1) { - obj.port = authority.substring(portBeginIndex + 1); - authority = authority.substring(0, portBeginIndex); - } - - // hostname - obj.hostname = authority; - } - - // pathname - regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi'); - match = regex.exec(str); - - let pathname; - - if (match) { - pathname = match[1]; - str = str.substring(regex.lastIndex); - } - if (pathname) { - if (!pathname.startsWith('/')) { - pathname = `/${pathname}`; - } - } else { - pathname = '/'; - } - obj.pathname = pathname; - - // query - if (str.startsWith('?')) { - let hashBeginIndex = str.indexOf('#', 1); - - if (hashBeginIndex === -1) { - hashBeginIndex = str.length; - } - obj.search = str.substring(0, hashBeginIndex); - str = str.substring(hashBeginIndex); - } else { - obj.search = ''; // Google Chrome - } - - // fragment - obj.hash = str.startsWith('#') ? str : ''; - - /* eslint-enable no-param-reassign */ - - return obj; -} - -/** - * Parses a specific URI which (supposedly) references a Jitsi Meet resource - * (location). - * - * @param {(string|undefined)} uri - The URI to parse which (supposedly) - * references a Jitsi Meet resource (location). - * @returns {{ - * room: (string|undefined) - * }} - */ -export function _parseURIString(uri: ?string) { - if (typeof uri !== 'string') { - return undefined; - } - - const obj - = _parseStandardURIString( - _fixURIStringHierPart(_fixURIStringScheme(uri))); - - // Add the properties that are specific to a Jitsi Meet resource (location) - // such as contextRoot, room: - - // contextRoot - obj.contextRoot = getLocationContextRoot(obj); - - // The room (name) is the last component of pathname. - const { pathname } = obj; - - obj.room = pathname.substring(pathname.lastIndexOf('/') + 1) || undefined; - - return obj; -} diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js index cd11288a0b..6eb0ea97d8 100644 --- a/react/features/app/functions.web.js +++ b/react/features/app/functions.web.js @@ -71,8 +71,6 @@ const _INTERCEPT_COMPONENT_RULES = [ } ]; -export { getLocationContextRoot, _parseURIString } from './functions.native'; - /** * Determines which route is to be rendered in order to depict a specific Redux * store. diff --git a/react/features/base/connection/actions.native.js b/react/features/base/connection/actions.native.js index e8d5f7a3d1..08374831d5 100644 --- a/react/features/base/connection/actions.native.js +++ b/react/features/base/connection/actions.native.js @@ -1,9 +1,11 @@ /* @flow */ +import _ from 'lodash'; import type { Dispatch } from 'redux'; import { conferenceWillLeave } from '../conference'; import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet'; +import { parseStandardURIString } from '../util'; import { CONNECTION_DISCONNECTED, @@ -21,29 +23,8 @@ import { export function connect() { return (dispatch: Dispatch<*>, getState: Function) => { const state = getState(); - let { options } = state['features/base/connection']; - - options = { - // Lib-jitsi-meet wants the config passed in multiple places and - // here is the latest one I have discovered. - ...state['features/base/config'], - - // TODO It is probable that config should override the options that - // have been automatically constructed by the app. Unfortunately, - // config may specify URLs such as bosh at the time of this writing - // which react-native cannot parse (because they do not have a - // protocol/scheme). - ...options - }; - + const options = _constructOptions(state); const { issuer, jwt } = state['features/jwt']; - const { room } = state['features/base/conference']; - - // XXX The Jitsi Meet deployments require the room argument to be in - // lower case at the time of this writing but, unfortunately, they do - // not ignore case themselves. - options.bosh += room ? `?room=${room.toLowerCase()}` : ''; - const connection = new JitsiMeetJS.JitsiConnection( options.appId, @@ -202,6 +183,51 @@ export function connectionFailed( }; } +/** + * Constructs options to be passed to the constructor of {@code JitsiConnection} + * based on the redux state. + * + * @param {Object} state - The redux state. + * @returns {Object} The options to be passed to the constructor of + * {@code JitsiConnection}. + */ +function _constructOptions(state) { + const defaultOptions = state['features/base/connection'].options; + const options = _.merge( + {}, + defaultOptions, + + // Lib-jitsi-meet wants the config passed in multiple places and here is + // the latest one I have discovered. + state['features/base/config'], + ); + let { bosh } = options; + + if (bosh) { + // Append room to the URL's search. + const { room } = state['features/base/conference']; + + // XXX The Jitsi Meet deployments require the room argument to be in + // lower case at the time of this writing but, unfortunately, they do + // not ignore case themselves. + room && (bosh += `?room=${room.toLowerCase()}`); + + // XXX By default, config.js does not add a protocol to the BOSH URL. + // Which trips React Native. Make sure there is a protocol in order to + // satisfy React Native. + if (bosh !== defaultOptions.bosh + && !parseStandardURIString(bosh).protocol) { + const { protocol } = parseStandardURIString(defaultOptions.bosh); + + protocol && (bosh = protocol + bosh); + } + + options.bosh = bosh; + } + + return options; +} + /** * Closes connection. * diff --git a/react/features/base/connection/reducer.js b/react/features/base/connection/reducer.js index 3ba42f9e85..035e383fd3 100644 --- a/react/features/base/connection/reducer.js +++ b/react/features/base/connection/reducer.js @@ -1,6 +1,7 @@ /* @flow */ import { assign, ReducerRegistry, set } from '../redux'; +import { parseURIString } from '../util'; import { CONNECTION_DISCONNECTED, @@ -119,38 +120,38 @@ function _connectionWillConnect( /** * Constructs options to be passed to the constructor of {@code JitsiConnection} - * based on a specific domain. + * based on a specific location URL. * - * @param {string} domain - The domain with which the returned options are to be - * populated. + * @param {string} locationURL - The location URL with which the returned + * options are to be constructed. * @private - * @returns {Object} + * @returns {Object} The options to be passed to the constructor of + * {@code JitsiConnection} based on the location URL. */ -function _constructOptions(domain: string) { +function _constructOptions(locationURL: URL) { + const locationURI = parseURIString(locationURL.href); + // FIXME The HTTPS scheme for the BOSH URL works with meet.jit.si on both // mobile & Web. It also works with beta.meet.jit.si on Web. Unfortunately, // it doesn't work with beta.meet.jit.si on mobile. Temporarily, use the // HTTP scheme for the BOSH URL with beta.meet.jit.si on mobile. - let boshProtocol; + let { protocol } = locationURI; + const domain = locationURI.hostname; - if (domain === 'beta.meet.jit.si') { - if (typeof window === 'object') { - const windowLocation = window.location; + if (!protocol && domain === 'beta.meet.jit.si') { + const windowLocation = window.location; - if (windowLocation) { - // React Native doesn't have a window.location at the time of - // this writing, let alone a window.location.protocol. - boshProtocol = windowLocation.protocol; - } - } - boshProtocol || (boshProtocol = 'http:'); + windowLocation && (protocol = windowLocation.protocol); + protocol || (protocol = 'http:'); } // Default to the HTTPS scheme for the BOSH URL. - boshProtocol || (boshProtocol = 'https:'); + protocol || (protocol = 'https:'); return { - bosh: `${String(boshProtocol)}//${domain}/http-bind`, + bosh: + `${String(protocol)}//${domain}${locationURI.contextRoot || '/' + }http-bind`, hosts: { domain, @@ -176,6 +177,6 @@ function _setLocationURL( { locationURL }: { locationURL: ?URL }) { return assign(state, { locationURL, - options: locationURL ? _constructOptions(locationURL.host) : undefined + options: locationURL ? _constructOptions(locationURL) : undefined }); } diff --git a/react/features/base/util/index.js b/react/features/base/util/index.js index bce5856634..001b03d3f2 100644 --- a/react/features/base/util/index.js +++ b/react/features/base/util/index.js @@ -1,2 +1,3 @@ export * from './loadScript'; export * from './randomUtil'; +export * from './uri'; diff --git a/react/features/base/util/uri.js b/react/features/base/util/uri.js new file mode 100644 index 0000000000..efb6ee03ef --- /dev/null +++ b/react/features/base/util/uri.js @@ -0,0 +1,264 @@ +/* @flow */ + +/** + * The {@link RegExp} pattern of the authority of a URI. + * + * @private + * @type {string} + */ +const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)'; + +/** + * The {@link RegExp} pattern of the path of a URI. + * + * @private + * @type {string} + */ +const _URI_PATH_PATTERN = '([^?#]*)'; + +/** + * The {@link RegExp} pattern of the protocol of a URI. + * + * @private + * @type {string} + */ +const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)'; + +/** + * Fixes the hier-part of a specific URI (string) so that the URI is well-known. + * For example, certain Jitsi Meet deployments are not conventional but it is + * possible to translate their URLs into conventional. + * + * @param {string} uri - The URI (string) to fix the hier-part of. + * @private + * @returns {string} + */ +function _fixURIStringHierPart(uri) { + // Rewrite the specified URL in order to handle special cases such as + // hipchat.com and enso.me which do not follow the common pattern of most + // Jitsi Meet deployments. + + // hipchat.com + let regex + = new RegExp( + `^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`, + 'gi'); + let match = regex.exec(uri); + + if (!match) { + // enso.me + regex + = new RegExp( + `^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`, + 'gi'); + match = regex.exec(uri); + } + if (match) { + /* eslint-disable no-param-reassign, prefer-template */ + + uri + = match[1] /* protocol */ + + '//enso.hipchat.me/' + + uri.substring(regex.lastIndex); /* room (name) */ + + /* eslint-enable no-param-reassign, prefer-template */ + } + + return uri; +} + +/** + * Fixes the scheme part of a specific URI (string) so that it contains a + * well-known scheme such as HTTP(S). For example, the mobile app implements an + * app-specific URI scheme in addition to Universal Links. The app-specific + * scheme may precede or replace the well-known scheme. In such a case, dealing + * with the app-specific scheme only complicates the logic and it is simpler to + * get rid of it (by translating the app-specific scheme into a well-known + * scheme). + * + * @param {string} uri - The URI (string) to fix the scheme of. + * @private + * @returns {string} + */ +function _fixURIStringScheme(uri: string) { + const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi'); + const match = regex.exec(uri); + + if (match) { + // As an implementation convenience, pick up the last scheme and make + // sure that it is a well-known one. + let protocol = match[match.length - 1].toLowerCase(); + + if (protocol !== 'http:' && protocol !== 'https:') { + protocol = 'https:'; + } + + /* eslint-disable no-param-reassign */ + + uri = uri.substring(regex.lastIndex); + if (uri.startsWith('//')) { + // The specified URL was not a room name only, it contained an + // authority. + uri = protocol + uri; + } + + /* eslint-enable no-param-reassign */ + } + + return uri; +} + +/** + * Gets the (Web application) context root defined by a specific location (URI). + * + * @param {Object} location - The location (URI) which defines the (Web + * application) context root. + * @public + * @returns {string} - The (Web application) context root defined by the + * specified {@code location} (URI). + */ +export function getLocationContextRoot(location: Object) { + const pathname = location.pathname; + const contextRootEndIndex = pathname.lastIndexOf('/'); + + return ( + contextRootEndIndex === -1 + ? '/' + : pathname.substring(0, contextRootEndIndex + 1)); +} + +/** + * Parses a specific URI string into an object with the well-known properties of + * the {@link Location} and/or {@link URL} interfaces implemented by Web + * browsers. The parsing attempts to be in accord with IETF's RFC 3986. + * + * @param {string} str - The URI string to parse. + * @public + * @returns {{ + * hash: string, + * host: (string|undefined), + * hostname: (string|undefined), + * pathname: string, + * port: (string|undefined), + * protocol: (string|undefined), + * search: string + * }} + */ +export function parseStandardURIString(str: string) { + /* eslint-disable no-param-reassign */ + + const obj = {}; + + let regex; + let match; + + // protocol + regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi'); + match = regex.exec(str); + if (match) { + obj.protocol = match[1].toLowerCase(); + str = str.substring(regex.lastIndex); + } + + // authority + regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi'); + match = regex.exec(str); + if (match) { + let authority = match[1].substring(/* // */ 2); + + str = str.substring(regex.lastIndex); + + // userinfo + const userinfoEndIndex = authority.indexOf('@'); + + if (userinfoEndIndex !== -1) { + authority = authority.substring(userinfoEndIndex + 1); + } + + obj.host = authority; + + // port + const portBeginIndex = authority.lastIndexOf(':'); + + if (portBeginIndex !== -1) { + obj.port = authority.substring(portBeginIndex + 1); + authority = authority.substring(0, portBeginIndex); + } + + // hostname + obj.hostname = authority; + } + + // pathname + regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi'); + match = regex.exec(str); + + let pathname; + + if (match) { + pathname = match[1]; + str = str.substring(regex.lastIndex); + } + if (pathname) { + if (!pathname.startsWith('/')) { + pathname = `/${pathname}`; + } + } else { + pathname = '/'; + } + obj.pathname = pathname; + + // query + if (str.startsWith('?')) { + let hashBeginIndex = str.indexOf('#', 1); + + if (hashBeginIndex === -1) { + hashBeginIndex = str.length; + } + obj.search = str.substring(0, hashBeginIndex); + str = str.substring(hashBeginIndex); + } else { + obj.search = ''; // Google Chrome + } + + // fragment + obj.hash = str.startsWith('#') ? str : ''; + + /* eslint-enable no-param-reassign */ + + return obj; +} + +/** + * Parses a specific URI which (supposedly) references a Jitsi Meet resource + * (location). + * + * @param {(string|undefined)} uri - The URI to parse which (supposedly) + * references a Jitsi Meet resource (location). + * @public + * @returns {{ + * room: (string|undefined) + * }} + */ +export function parseURIString(uri: ?string) { + if (typeof uri !== 'string') { + return undefined; + } + + const obj + = parseStandardURIString( + _fixURIStringHierPart(_fixURIStringScheme(uri))); + + // Add the properties that are specific to a Jitsi Meet resource (location) + // such as contextRoot, room: + + // contextRoot + obj.contextRoot = getLocationContextRoot(obj); + + // The room (name) is the last component of pathname. + const { pathname } = obj; + + obj.room = pathname.substring(pathname.lastIndexOf('/') + 1) || undefined; + + return obj; +}