From bd7c9473e7925990b4169f182a1aedd3a6f6ca24 Mon Sep 17 00:00:00 2001 From: Lyubo Marinov Date: Wed, 9 May 2018 23:45:24 -0500 Subject: [PATCH] [Android] Fix possible TypeError in multi-JitsiMeetView SDK consumers If multiple JitsiMeetView instances are created (not necessarily existing at once), it's possible to hit a TypeError when reading the React Component props of the currently mounted App. Anyway, in certain places we're already protecting against that out of abundance of caution so it makes no sense to not protect everywhere. --- react/features/app/functions.any.js | 28 +++++++++++++++++++ react/features/app/functions.native.js | 1 + react/features/app/functions.web.js | 4 ++- react/features/invite/functions.js | 8 ++---- react/features/invite/middleware.native.js | 19 ++++--------- .../mobile/external-api/middleware.js | 19 ++++--------- .../mobile/picture-in-picture/actions.js | 6 ++-- .../EnterPictureInPictureToolbarButton.js | 5 ++-- react/features/welcome/functions.js | 22 +++++++-------- 9 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 react/features/app/functions.any.js diff --git a/react/features/app/functions.any.js b/react/features/app/functions.any.js new file mode 100644 index 000000000..38fa6d14d --- /dev/null +++ b/react/features/app/functions.any.js @@ -0,0 +1,28 @@ +// @flow + +import { toState } from '../base/redux'; + +/** + * Gets the value of a specific React {@code Component} prop of the currently + * mounted {@link App}. + * + * @param {Function|Object} stateful - The redux store or {@code getState} + * function. + * @param {string} propName - The name of the React {@code Component} prop of + * the currently mounted {@code App} to get. + * @returns {*} The value of the specified React {@code Compoennt} prop of the + * currently mounted {@code App}. + */ +export function getAppProp(stateful: Function | Object, propName: string) { + const state = toState(stateful)['features/app']; + + if (state) { + const { app } = state; + + if (app) { + return app.props[propName]; + } + } + + return undefined; +} diff --git a/react/features/app/functions.native.js b/react/features/app/functions.native.js index cbe3ede67..0da77a8f1 100644 --- a/react/features/app/functions.native.js +++ b/react/features/app/functions.native.js @@ -2,6 +2,7 @@ import { NativeModules } from 'react-native'; +export * from './functions.any'; export * from './getRouteToRender'; /** diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js index 322ae6133..86bd6a424 100644 --- a/react/features/app/functions.web.js +++ b/react/features/app/functions.web.js @@ -1,4 +1,4 @@ -/* @flow */ +// @flow import { toState } from '../base/redux'; import { getDeepLinkingPage } from '../deep-linking'; @@ -49,6 +49,8 @@ const _INTERCEPT_COMPONENT_RULES = [ } ]; +export * from './functions.any'; + /** * Determines which route is to be rendered in order to depict a specific redux * store. diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index bf690e926..3faa4b95b 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -1,5 +1,6 @@ // @flow +import { getAppProp } from '../app'; import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants'; import { doGetJSON } from '../base/util'; @@ -282,8 +283,7 @@ export function isAddPeopleEnabled(state: Object): boolean { // XXX The mobile/react-native app is capable of disabling the // adding/inviting of people in the current conference. Anyway, the // Web/React app does not have that capability so default appropriately. - const { app } = state['features/app']; - const addPeopleEnabled = app && app.props.addPeopleEnabled; + const addPeopleEnabled = getAppProp(state, 'addPeopleEnabled'); return ( (typeof addPeopleEnabled === 'undefined') @@ -313,9 +313,7 @@ export function isDialOutEnabled(state: Object): boolean { // XXX The mobile/react-native app is capable of disabling of dial-out. // Anyway, the Web/React app does not have that capability so default // appropriately. - const { app } = state['features/app']; - - dialOutEnabled = app && app.props.dialoOutEnabled; + dialOutEnabled = getAppProp(state, 'dialOutEnabled'); return ( (typeof dialOutEnabled === 'undefined') || Boolean(dialOutEnabled)); diff --git a/react/features/invite/middleware.native.js b/react/features/invite/middleware.native.js index 0e7cc851e..9226cd6a6 100644 --- a/react/features/invite/middleware.native.js +++ b/react/features/invite/middleware.native.js @@ -4,7 +4,7 @@ import i18next from 'i18next'; import { NativeEventEmitter, NativeModules } from 'react-native'; import { MiddlewareRegistry } from '../base/redux'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, getAppProp } from '../app'; import { invite } from './actions'; import { @@ -107,21 +107,15 @@ function _appWillMount({ dispatch, getState }, next, action) { * @private * @returns {*} The value returned by {@code next(action)}. */ -function _beginAddPeople({ getState }, next, action) { +function _beginAddPeople(store, next, action) { const result = next(action); // The JavaScript App needs to provide uniquely identifying information to // the native Invite module so that the latter may match the former to the // native JitsiMeetView which hosts it. - const { app } = getState()['features/app']; + const externalAPIScope = getAppProp(store, 'externalAPIScope'); - if (app) { - const { externalAPIScope } = app.props; - - if (externalAPIScope) { - Invite.beginAddPeople(externalAPIScope); - } - } + externalAPIScope && Invite.beginAddPeople(externalAPIScope); return result; } @@ -139,8 +133,7 @@ function _onInvite({ addPeopleControllerScope, externalAPIScope, invitees }) { // If there are multiple JitsiMeetView instances alive, they will all get // the event, since there is a single bridge, so make sure we don't act if // the event is not for us. - if (getState()['features/app'].app.props.externalAPIScope - !== externalAPIScope) { + if (getAppProp(getState, 'externalAPIScope') !== externalAPIScope) { return; } @@ -167,7 +160,7 @@ function _onPerformQuery( // If there are multiple JitsiMeetView instances alive, they will all get // the event, since there is a single bridge, so make sure we don't act if // the event is not for us. - if (state['features/app'].app.props.externalAPIScope !== externalAPIScope) { + if (getAppProp(state, 'externalAPIScope') !== externalAPIScope) { return; } diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js index 29f5003f8..de8b5d299 100644 --- a/react/features/mobile/external-api/middleware.js +++ b/react/features/mobile/external-api/middleware.js @@ -1,7 +1,8 @@ -/* @flow */ +// @flow import { NativeModules } from 'react-native'; +import { getAppProp } from '../../app'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, @@ -215,22 +216,14 @@ function _sendConferenceFailedOnConnectionError(store, action) { * @private * @returns {void} */ -function _sendEvent( - { getState }: { getState: Function }, - name: string, - data: Object) { +function _sendEvent(store: Object, name: string, data: Object) { // The JavaScript App needs to provide uniquely identifying information to // the native ExternalAPI module so that the latter may match the former to // the native JitsiMeetView which hosts it. - const { app } = getState()['features/app']; + const externalAPIScope = getAppProp(store, 'externalAPIScope'); - if (app) { - const { externalAPIScope } = app.props; - - if (externalAPIScope) { - NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope); - } - } + externalAPIScope + && NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope); } /** diff --git a/react/features/mobile/picture-in-picture/actions.js b/react/features/mobile/picture-in-picture/actions.js index 913e91120..4bec87d67 100644 --- a/react/features/mobile/picture-in-picture/actions.js +++ b/react/features/mobile/picture-in-picture/actions.js @@ -2,6 +2,7 @@ import { NativeModules } from 'react-native'; +import { getAppProp } from '../../app'; import { Platform } from '../../base/react'; import { ENTER_PICTURE_IN_PICTURE } from './actionTypes'; @@ -18,13 +19,10 @@ import { ENTER_PICTURE_IN_PICTURE } from './actionTypes'; */ export function enterPictureInPicture() { return (dispatch: Dispatch, getState: Function) => { - const state = getState(); - const { app } = state['features/app']; - // XXX At the time of this writing this action can only be dispatched by // the button which is on the conference view, which means that it's // fine to enter PiP mode. - if (app && app.props.pictureInPictureEnabled) { + if (getAppProp(getState, 'pictureInPictureEnabled')) { const { PictureInPicture } = NativeModules; const p = Platform.OS === 'android' diff --git a/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js b/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js index 497404983..b86dbae07 100644 --- a/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js +++ b/react/features/mobile/picture-in-picture/components/EnterPictureInPictureToolbarButton.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; +import { getAppProp } from '../../../app'; import { ToolbarButton } from '../../../toolbox'; import { enterPictureInPicture } from '../actions'; @@ -93,8 +94,6 @@ function _mapDispatchToProps(dispatch) { * }} */ function _mapStateToProps(state) { - const { app } = state['features/app']; - return { /** @@ -104,7 +103,7 @@ function _mapStateToProps(state) { * @type {boolean} */ _pictureInPictureEnabled: - Boolean(app && app.props.pictureInPictureEnabled) + Boolean(getAppProp(state, 'pictureInPictureEnabled')) }; } diff --git a/react/features/welcome/functions.js b/react/features/welcome/functions.js index 760a2c9ce..47e761ca0 100644 --- a/react/features/welcome/functions.js +++ b/react/features/welcome/functions.js @@ -1,5 +1,6 @@ -/* @flow */ +// @flow +import { getAppProp } from '../app'; import { toState } from '../base/redux'; declare var APP: Object; @@ -12,12 +13,12 @@ export * from './roomnameGenerator'; * (e.g. programmatically via the Jitsi Meet SDK for Android and iOS). Not to be * confused with {@link isWelcomePageUserEnabled}. * - * @param {Object|Function} stateOrGetState - The redux state or - * {@link getState} function. + * @param {Function|Object} stateful - The redux state or {@link getState} + * function. * @returns {boolean} If the {@code WelcomePage} is enabled by the app, then * {@code true}; otherwise, {@code false}. */ -export function isWelcomePageAppEnabled(stateOrGetState: Object | Function) { +export function isWelcomePageAppEnabled(stateful: Function | Object) { let b; if (navigator.product === 'ReactNative') { @@ -28,9 +29,7 @@ export function isWelcomePageAppEnabled(stateOrGetState: Object | Function) { // - Enabling/disabling the Welcome page on Web historically // automatically redirects to a random room and that does not make sense // on mobile (right now). - const { app } = toState(stateOrGetState)['features/app']; - - b = Boolean(app && app.props.welcomePageEnabled); + b = Boolean(getAppProp(stateful, 'welcomePageEnabled')); } else { b = true; } @@ -43,15 +42,14 @@ export function isWelcomePageAppEnabled(stateOrGetState: Object | Function) { * herself or through her deployment config(uration). Not to be confused with * {@link isWelcomePageAppEnabled}. * - * @param {Object|Function} stateOrGetState - The redux state or - * {@link getState} function. + * @param {Function|Object} stateful - The redux state or {@link getState} + * function. * @returns {boolean} If the {@code WelcomePage} is enabled by the user, then * {@code true}; otherwise, {@code false}. */ -export function isWelcomePageUserEnabled(stateOrGetState: Object | Function) { +export function isWelcomePageUserEnabled(stateful: Function | Object) { return ( typeof APP === 'undefined' ? true - : toState(stateOrGetState)['features/base/config'] - .enableWelcomePage); + : toState(stateful)['features/base/config'].enableWelcomePage); }