From 320e67baa1d3b78229daa2331a5917cc8b63a5a5 Mon Sep 17 00:00:00 2001 From: Lyubo Marinov Date: Fri, 26 May 2017 17:11:33 -0500 Subject: [PATCH] Fix the initialization of the (external) API The counterpart of the external API in the Jitsi Meet Web app uses the search URL param jwt to heuristically detect that the Web app is very likely embedded (as an iframe) and, consequently, needs to forcefully enable itself. It was looking at whether there was a JSON Web Token (JWT) but that logic got broken when the JWT support was rewritten because the check started happening before the search URL param jwt was parsed. --- app.js | 42 +++++++++-------- modules/API/API.js | 17 +++++-- modules/keyboardshortcut/keyboardshortcut.js | 18 +++----- react/features/app/actions.js | 43 ++++++++---------- react/features/app/components/AbstractApp.js | 2 +- react/features/app/components/App.web.js | 12 ----- react/features/app/functions.web.js | 47 ------------------- react/features/app/index.js | 5 -- react/features/base/config/parseURLParams.js | 2 +- react/features/base/i18n/index.js | 2 + react/features/base/i18n/middleware.js | 48 ++++++++++++++++++++ react/features/base/logging/middleware.js | 26 +++++++++++ react/features/jwt/functions.js | 16 +++++++ react/features/jwt/index.js | 1 + react/features/jwt/middleware.js | 5 +- 15 files changed, 160 insertions(+), 126 deletions(-) create mode 100644 react/features/base/i18n/middleware.js create mode 100644 react/features/jwt/functions.js diff --git a/app.js b/app.js index 0d82d480c..5bd759360 100644 --- a/app.js +++ b/app.js @@ -15,15 +15,25 @@ import 'aui-experimental-css'; window.toastr = require("toastr"); -import UI from "./modules/UI/UI"; -import settings from "./modules/settings/Settings"; import conference from './conference'; import API from './modules/API'; - -import translation from "./modules/translation/translation"; +import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut'; import remoteControl from "./modules/remotecontrol/RemoteControl"; +import settings from "./modules/settings/Settings"; +import translation from "./modules/translation/translation"; +import UI from "./modules/UI/UI"; const APP = { + API, + conference, + + /** + * After the APP has been initialized provides utility methods for dealing + * with the conference room URL(address). + * @type ConferenceUrl + */ + ConferenceUrl: null, + // Used by do_external_connect.js if we receive the attach data after // connect was already executed. status property can be "initialized", // "ready" or "connecting". We are interested in "ready" status only which @@ -34,33 +44,29 @@ const APP = { status: "initialized", handler: null }, + connection: null, + // Used for automated performance tests connectionTimes: { "index.loaded": window.indexLoadedTime }, - UI, - settings, - conference, - translation, + keyboardshortcut, + /** * The log collector which captures JS console logs for this app. * @type {LogCollector} */ logCollector: null, + /** * Indicates if the log collector has been started (it will not be started * if the welcome page is displayed). */ logCollectorStarted : false, - /** - * After the APP has been initialized provides utility methods for dealing - * with the conference room URL(address). - * @type ConferenceUrl - */ - ConferenceUrl : null, - connection: null, - API, - remoteControl + remoteControl, + settings, + translation, + UI }; // TODO The execution of the mobile app starts from react/index.native.js. @@ -69,6 +75,6 @@ const APP = { // because we are at the beginning of introducing React into the Web app, allow // the execution of the Web app to start from app.js in order to reduce the // complexity of the beginning step. -require('./react'); +import './react'; module.exports = APP; diff --git a/modules/API/API.js b/modules/API/API.js index a429e435c..1a884a35c 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -1,4 +1,5 @@ import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; +import { parseJWTFromURLParams } from '../../react/features/jwt'; import { getJitsiMeetTransport } from '../transport'; import { API_ID } from './constants'; @@ -74,7 +75,15 @@ function onDesktopSharingEnabledChanged(enabled = false) { * @returns {boolean} */ function shouldBeEnabled() { - return typeof API_ID === 'number'; + return ( + typeof API_ID === 'number' + + // XXX Enable the API when a JSON Web Token (JWT) is specified in + // the location/URL because then it is very likely that the Jitsi + // Meet (Web) app is being used by an external/wrapping (Web) app + // and, consequently, the latter will need to communicate with the + // former. (The described logic is merely a heuristic though.) + || parseJWTFromURLParams()); } /** @@ -102,12 +111,10 @@ class API { * sends a message to the external application that API is initialized. * * @param {Object} options - Optional parameters. - * @param {boolean} options.forceEnable - True to forcefully enable the - * module. * @returns {void} */ - init({ forceEnable } = {}) { - if (!shouldBeEnabled() && !forceEnable) { + init() { + if (!shouldBeEnabled()) { return; } diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js index cd50e94c8..b1b4a26e0 100644 --- a/modules/keyboardshortcut/keyboardshortcut.js +++ b/modules/keyboardshortcut/keyboardshortcut.js @@ -1,8 +1,6 @@ /* global APP, $, JitsiMeetJS, interfaceConfig */ -import { - toggleDialog -} from '../../react/features/base/dialog'; +import { toggleDialog } from '../../react/features/base/dialog'; import { SpeakerStats } from '../../react/features/speaker-stats'; /** @@ -17,7 +15,6 @@ let keyboardShortcutDialog = null; * triggered _only_ with a shortcut. */ function initGlobalShortcuts() { - KeyboardShortcut.registerShortcut("ESCAPE", null, function() { showKeyboardShortcutsPanel(false); }); @@ -57,19 +54,16 @@ function initGlobalShortcuts() { */ function showKeyboardShortcutsPanel(show) { if (show - && !APP.UI.messageHandler.isDialogOpened() - && keyboardShortcutDialog === null) { - + && !APP.UI.messageHandler.isDialogOpened() + && keyboardShortcutDialog === null) { let msg = $('#keyboard-shortcuts').html(); let buttons = { Close: true }; keyboardShortcutDialog = APP.UI.messageHandler.openDialog( 'keyboardShortcuts.keyboardShortcuts', msg, true, buttons); - } else { - if (keyboardShortcutDialog !== null) { - keyboardShortcutDialog.close(); - keyboardShortcutDialog = null; - } + } else if (keyboardShortcutDialog !== null) { + keyboardShortcutDialog.close(); + keyboardShortcutDialog = null; } } diff --git a/react/features/app/actions.js b/react/features/app/actions.js index 1ba255111..faa42bbc7 100644 --- a/react/features/app/actions.js +++ b/react/features/app/actions.js @@ -4,23 +4,9 @@ import { setConfig } from '../base/config'; import { loadConfig } from '../base/lib-jitsi-meet'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes'; -import { - _getRouteToRender, - _parseURIString, - init -} from './functions'; +import { _getRouteToRender, _parseURIString } from './functions'; -/** - * Temporary solution. Should dispatch actions related to initial settings of - * the app like setting log levels, reading the config parameters from query - * string etc. - * - * @returns {Function} - */ -export function appInit() { - return (dispatch: Dispatch<*>, getState: Function) => - init(getState()); -} +declare var APP: Object; /** * Triggers an in-app navigation to a specific route. Allows navigation to be @@ -28,7 +14,7 @@ export function appInit() { * * @param {(string|undefined)} uri - The URI to which to navigate. It may be a * full URL with an HTTP(S) scheme, a full or partial URI with the app-specific - * sheme, or a mere room name. + * scheme, or a mere room name. * @returns {Function} */ export function appNavigate(uri: ?string) { @@ -180,9 +166,20 @@ function _appNavigateToOptionalLocation( * }} */ export function appWillMount(app) { - return { - type: APP_WILL_MOUNT, - app + return (dispatch: Dispatch<*>) => { + dispatch({ + type: APP_WILL_MOUNT, + app + }); + + // TODO There was a redux action creator appInit which I did not like + // because we already had the redux action creator appWillMount and, + // respectively, the redux action APP_WILL_MOUNT. So I set out to remove + // appInit and managed to move everything it was doing but the + // following. Which is not extremely bad because we haven't moved the + // API module into its own feature yet so we're bound to work on that in + // the future. + typeof APP === 'object' && APP.API.init(); }; } @@ -205,7 +202,7 @@ export function appWillUnmount(app) { /** * Loads config.js from a specific host. * - * @param {Object} location - The loction URI which specifies the host to load + * @param {Object} location - The location URI which specifies the host to load * the config.js from. * @returns {Promise} */ @@ -224,9 +221,9 @@ function _loadConfig(location: Object) { } /** - * Navigates to a route in accord with a specific Redux state. + * Navigates to a route in accord with a specific redux state. * - * @param {Object} state - The Redux state which determines/identifies the route + * @param {Object} state - The redux state which determines/identifies the route * to navigate to. * @private * @returns {void} diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index 1170da7e0..161ce95fc 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -84,7 +84,7 @@ export class AbstractApp extends Component { // business logic in the React Component (i.e. UI) AbstractApp now. let localParticipant; - if (typeof APP !== 'undefined') { + if (typeof APP === 'object') { localParticipant = { avatarID: APP.settings.getAvatarId(), avatarURL: APP.settings.getAvatarUrl(), diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index 62f1abbe9..0488f4d23 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -1,4 +1,3 @@ -import { appInit } from '../actions'; import { AbstractApp } from './AbstractApp'; import { getLocationContextRoot } from '../functions'; @@ -38,17 +37,6 @@ export class App extends AbstractApp { }; } - /** - * Inits the app before component will mount. - * - * @inheritdoc - */ - componentWillMount(...args) { - super.componentWillMount(...args); - - this._getStore().dispatch(appInit()); - } - /** * Gets a Location object from the window with information about the current * location of the document. diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js index ede39654f..cd11288a0 100644 --- a/react/features/app/functions.web.js +++ b/react/features/app/functions.web.js @@ -1,9 +1,6 @@ /* @flow */ -import Logger from 'jitsi-meet-logger'; - import { isRoomValid } from '../base/conference'; -import JitsiMeetJS from '../base/lib-jitsi-meet'; import { Platform, RouteRegistry } from '../base/react'; import { Conference } from '../conference'; import { @@ -14,10 +11,6 @@ import { } from '../unsupported-browser'; import { WelcomePage } from '../welcome'; -import KeyboardShortcut - from '../../../modules/keyboardshortcut/keyboardshortcut'; -import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage'; - declare var APP: Object; declare var interfaceConfig: Object; declare var loggingConfig: Object; @@ -106,46 +99,6 @@ export function _getRouteToRender(stateOrGetState: Object | Function) { return route; } -/** - * Temporary solution. Later we'll get rid of global APP and set its properties - * in redux store. - * - * @param {Object} state - Snapshot of current state of redux store. - * @returns {void} - */ -export function init(state: Object) { - _initLogging(); - - APP.keyboardshortcut = KeyboardShortcut; - - const { jwt } = state['features/jwt']; - - // Force enable the API if jwt token is passed because most probably - // jitsi meet is displayed inside of wrapper that will need to communicate - // with jitsi meet. - APP.API.init(jwt ? { forceEnable: true } : undefined); - - APP.translation.init(); -} - -/** - * Initializes logging in the app. - * - * @private - * @returns {void} - */ -function _initLogging() { - // Create the LogCollector and register it as the global log transport. It - // is done early to capture as much logs as possible. Captured logs will be - // cached, before the JitsiMeetLogStorage gets ready (statistics module is - // initialized). - if (!APP.logCollector && !loggingConfig.disableLogCollector) { - APP.logCollector = new Logger.LogCollector(new JitsiMeetLogStorage()); - Logger.addGlobalTransport(APP.logCollector); - JitsiMeetJS.addGlobalLogTransport(APP.logCollector); - } -} - /** * Intercepts route components based on a {@link _INTERCEPT_COMPONENT_RULES}. * diff --git a/react/features/app/index.js b/react/features/app/index.js index cb41009ea..5d564613e 100644 --- a/react/features/app/index.js +++ b/react/features/app/index.js @@ -3,9 +3,4 @@ export * from './actionTypes'; export * from './components'; export * from './functions'; -// We need to import the jwt module in order to register the reducer and -// middleware, because the module is not used outside of this feature. -import '../jwt'; - import './reducer'; - diff --git a/react/features/base/config/parseURLParams.js b/react/features/base/config/parseURLParams.js index 11951a911..afc43f090 100644 --- a/react/features/base/config/parseURLParams.js +++ b/react/features/base/config/parseURLParams.js @@ -3,7 +3,7 @@ /** * Parses the parameters from the URL and returns them as a JS object. * - * @param {string} url - URL to parse. + * @param {string} url - The URL to parse. * @param {boolean} dontParse - If false or undefined some transformations * (for parsing the value as JSON) are going to be executed. * @param {string} source - Values - "hash"/"search" if "search" the parameters diff --git a/react/features/base/i18n/index.js b/react/features/base/i18n/index.js index 4fb07bd59..49b25f87a 100644 --- a/react/features/base/i18n/index.js +++ b/react/features/base/i18n/index.js @@ -4,3 +4,5 @@ export * from './functions'; // TODO Eventually (e.g. when the non-React Web app is rewritten into React), it // should not be necessary to export i18next. export { default as i18next } from './i18next'; + +import './middleware'; diff --git a/react/features/base/i18n/middleware.js b/react/features/base/i18n/middleware.js new file mode 100644 index 000000000..349e90036 --- /dev/null +++ b/react/features/base/i18n/middleware.js @@ -0,0 +1,48 @@ +/* @flow */ + +import { SET_CONFIG } from '../config'; +import { MiddlewareRegistry } from '../redux'; + +declare var APP: Object; + +/** + * The redux middleware of the feature base/i18n. + * + * @param {Store} store - The redux store. + * @returns {Function} + * @private + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case SET_CONFIG: + return _setConfig(store, next, action); + } + + return next(action); +}); + +/** + * Notifies the feature base/i18n that the action SET_CONFIG is being dispatched + * within a specific redux store. + * + * @param {Store} store - The redux store in which the specified action is being + * dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action SET_CONFIG which is being + * dispatched in the specified store. + * @private + * @returns {Object} The new state that is the result of the reduction of the + * specified action. + */ +function _setConfig({ dispatch, getState }, next, action) { + const oldValue = getState()['features/base/config']; + const result = next(action); + const newValue = getState()['features/base/config']; + + if (oldValue !== newValue && typeof APP === 'object') { + APP.translation.init(); + } + + return result; +} diff --git a/react/features/base/logging/middleware.js b/react/features/base/logging/middleware.js index 82b50855a..0507f6af7 100644 --- a/react/features/base/logging/middleware.js +++ b/react/features/base/logging/middleware.js @@ -6,6 +6,8 @@ import { APP_WILL_MOUNT } from '../../app'; import JitsiMeetJS, { LIB_WILL_INIT } from '../lib-jitsi-meet'; import { MiddlewareRegistry } from '../redux'; +import JitsiMeetLogStorage from '../../../../modules/util/JitsiMeetLogStorage'; + import { SET_LOGGING_CONFIG } from './actionTypes'; declare var APP: Object; @@ -60,6 +62,28 @@ function _appWillMount({ getState }, next, action) { return next(action); } +/** + * Initializes logging in the app. + * + * @param {Object} loggingConfig - The configuration with which logging is to be + * initialized. + * @private + * @returns {void} + */ +function _initLogging(loggingConfig) { + // Create the LogCollector and register it as the global log transport. It + // is done early to capture as much logs as possible. Captured logs will be + // cached, before the JitsiMeetLogStorage gets ready (statistics module is + // initialized). + if (typeof APP === 'object' + && !APP.logCollector + && !loggingConfig.disableLogCollector) { + APP.logCollector = new Logger.LogCollector(new JitsiMeetLogStorage()); + Logger.addGlobalTransport(APP.logCollector); + JitsiMeetJS.addGlobalLogTransport(APP.logCollector); + } +} + /** * Notifies the feature base/logging that the action {@link LIB_WILL_INIT} is * being dispatched within a specific Redux {@code store}. @@ -102,6 +126,8 @@ function _setLoggingConfig({ getState }, next, action) { if (oldValue !== newValue) { _setLogLevels(Logger, newValue); _setLogLevels(JitsiMeetJS, newValue); + + _initLogging(newValue); } return result; diff --git a/react/features/jwt/functions.js b/react/features/jwt/functions.js new file mode 100644 index 000000000..8a88a41ce --- /dev/null +++ b/react/features/jwt/functions.js @@ -0,0 +1,16 @@ +/* @flow */ + +import { parseURLParams } from '../base/config'; + +/** + * Retrieves the JSON Web Token (JWT), if any, defined by a specific + * {@link URL}. + * + * @param {URL} url - The {@code URL} to parse and retrieve the JSON Web Token + * (JWT), if any, from. + * @returns {string} The JSON Web Token (JWT), if any, defined by the specified + * {@code url}; otherwise, {@code undefined}. + */ +export function parseJWTFromURLParams(url: URL = window.location) { + return parseURLParams(url, true, 'search').jwt; +} diff --git a/react/features/jwt/index.js b/react/features/jwt/index.js index dbf9c8839..eea8d7393 100644 --- a/react/features/jwt/index.js +++ b/react/features/jwt/index.js @@ -1,4 +1,5 @@ export * from './actions'; +export * from './functions'; import './middleware'; import './reducer'; diff --git a/react/features/jwt/middleware.js b/react/features/jwt/middleware.js index fc3f37925..79f0ce72e 100644 --- a/react/features/jwt/middleware.js +++ b/react/features/jwt/middleware.js @@ -1,11 +1,12 @@ import jwtDecode from 'jwt-decode'; -import { parseURLParams, SET_CONFIG } from '../base/config'; +import { SET_CONFIG } from '../base/config'; import { SET_LOCATION_URL } from '../base/connection'; import { MiddlewareRegistry } from '../base/redux'; import { setJWT } from './actions'; import { SET_JWT } from './actionTypes'; +import { parseJWTFromURLParams } from './functions'; /** * Middleware to parse token data upon setting a new room URL. @@ -55,7 +56,7 @@ function _setConfigOrLocationURL({ dispatch, getState }, next, action) { let jwt; if (locationURL) { - jwt = parseURLParams(locationURL, true, 'search').jwt; + jwt = parseJWTFromURLParams(locationURL); } dispatch(setJWT(jwt));