mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-19 00:17:47 +00:00
It's still possible to disable it, but when not in an iframe, audio and video tracks will not be created. When in an iframe, it's ok to let it happen, since the host sit is the one where permissions need to be granted, thanks to permission delegation. Fixes: https://github.com/jitsi/jitsi-meet/issues/16262 Ref: https://zimzi.substack.com/p/jitsi-privacy-flaw-that-enables-one
455 lines
15 KiB
TypeScript
455 lines
15 KiB
TypeScript
// @ts-ignore
|
|
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
|
// eslint-disable-next-line lines-around-comment
|
|
// @ts-ignore
|
|
import { safeJsonParse } from '@jitsi/js-utils/json';
|
|
import { isEmpty, mergeWith, pick } from 'lodash-es';
|
|
|
|
import { IReduxState } from '../../app/types';
|
|
import { getLocalParticipant } from '../participants/functions';
|
|
import { isEmbedded } from '../util/embedUtils';
|
|
import { parseURLParams } from '../util/parseURLParams';
|
|
|
|
import { IConfig } from './configType';
|
|
import CONFIG_WHITELIST from './configWhitelist';
|
|
import {
|
|
DEFAULT_HELP_CENTRE_URL,
|
|
DEFAULT_PRIVACY_URL,
|
|
DEFAULT_TERMS_URL,
|
|
FEATURE_FLAGS,
|
|
_CONFIG_STORE_PREFIX
|
|
} from './constants';
|
|
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
|
|
import logger from './logger';
|
|
|
|
// XXX The function getRoomName is split out of
|
|
// functions.any.js because it is bundled in both app.bundle and
|
|
// do_external_connect, webpack 1 does not support tree shaking, and we don't
|
|
// want all functions to be bundled in do_external_connect.
|
|
export { default as getRoomName } from './getRoomName';
|
|
|
|
/**
|
|
* Create a "fake" configuration object for the given base URL. This is used in case the config
|
|
* couldn't be loaded in the welcome page, so at least we have something to try with.
|
|
*
|
|
* @param {string} baseURL - URL of the deployment for which we want the fake config.
|
|
* @returns {Object}
|
|
*/
|
|
export function createFakeConfig(baseURL: string) {
|
|
const url = new URL(baseURL);
|
|
|
|
return {
|
|
hosts: {
|
|
domain: url.hostname,
|
|
muc: `conference.${url.hostname}`
|
|
},
|
|
bosh: `${baseURL}http-bind`,
|
|
p2p: {
|
|
enabled: true
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the meeting region.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {string}
|
|
*/
|
|
export function getMeetingRegion(state: IReduxState) {
|
|
return state['features/base/config']?.deploymentInfo?.region || '';
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the SSRC-rewriting feature flag.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {boolean}
|
|
*/
|
|
export function getSsrcRewritingFeatureFlag(state: IReduxState) {
|
|
return getFeatureFlag(state, FEATURE_FLAGS.SSRC_REWRITING) ?? true;
|
|
}
|
|
|
|
/**
|
|
* Selector used to get a feature flag.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @param {string} featureFlag - The name of the feature flag.
|
|
* @returns {boolean}
|
|
*/
|
|
export function getFeatureFlag(state: IReduxState, featureFlag: string) {
|
|
const featureFlags = state['features/base/config']?.flags || {};
|
|
|
|
return featureFlags[featureFlag as keyof typeof featureFlags];
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the disableRemoveRaisedHandOnFocus.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {boolean}
|
|
*/
|
|
export function getDisableRemoveRaisedHandOnFocus(state: IReduxState) {
|
|
return state['features/base/config']?.raisedHands?.disableRemoveRaisedHandOnFocus || false;
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the disableLowerHandByModerator.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {boolean}
|
|
*/
|
|
export function getDisableLowerHandByModerator(state: IReduxState) {
|
|
return state['features/base/config']?.raisedHands?.disableLowerHandByModerator || false;
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the disableLowerHandNotification.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {boolean}
|
|
*/
|
|
export function getDisableLowerHandNotification(state: IReduxState) {
|
|
return state['features/base/config']?.raisedHands?.disableLowerHandNotification || true;
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the disableNextSpeakerNotification.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {boolean}
|
|
*/
|
|
export function getDisableNextSpeakerNotification(state: IReduxState) {
|
|
return state['features/base/config']?.raisedHands?.disableNextSpeakerNotification || false;
|
|
}
|
|
|
|
/**
|
|
* Selector used to get the endpoint used for fetching the recording.
|
|
*
|
|
* @param {Object} state - The global state.
|
|
* @returns {string}
|
|
*/
|
|
export function getRecordingSharingUrl(state: IReduxState) {
|
|
return state['features/base/config'].recordingSharingUrl;
|
|
}
|
|
|
|
/**
|
|
* Overrides JSON properties in {@code config} and
|
|
* {@code interfaceConfig} Objects with the values from {@code newConfig}.
|
|
* Overrides only the whitelisted keys.
|
|
*
|
|
* @param {Object} config - The config Object in which we'll be overriding
|
|
* properties.
|
|
* @param {Object} interfaceConfig - The interfaceConfig Object in which we'll
|
|
* be overriding properties.
|
|
* @param {Object} json - Object containing configuration properties.
|
|
* Destination object is selected based on root property name:
|
|
* {
|
|
* config: {
|
|
* // config.js properties here
|
|
* },
|
|
* interfaceConfig: {
|
|
* // interface_config.js properties here
|
|
* }
|
|
* }.
|
|
* @returns {void}
|
|
*/
|
|
export function overrideConfigJSON(config: IConfig, interfaceConfig: any, json: any) {
|
|
for (const configName of Object.keys(json)) {
|
|
let configObj;
|
|
|
|
if (configName === 'config') {
|
|
configObj = config;
|
|
} else if (configName === 'interfaceConfig') {
|
|
configObj = interfaceConfig;
|
|
}
|
|
if (configObj) {
|
|
const configJSON
|
|
= getWhitelistedJSON(configName as 'interfaceConfig' | 'config', json[configName]);
|
|
|
|
if (!isEmpty(configJSON)) {
|
|
logger.info(`Extending ${configName} with: ${JSON.stringify(configJSON)}`);
|
|
|
|
// eslint-disable-next-line arrow-body-style
|
|
mergeWith(configObj, configJSON, (oldValue, newValue) => {
|
|
|
|
// XXX We don't want to merge the arrays, we want to
|
|
// overwrite them.
|
|
return Array.isArray(oldValue) ? newValue : undefined;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* eslint-enable max-params, no-shadow */
|
|
|
|
/**
|
|
* Apply whitelist filtering for configs with whitelists.
|
|
* Only extracts overridden values for keys we allow to be overridden.
|
|
*
|
|
* @param {string} configName - The config name, one of config or interfaceConfig.
|
|
* @param {Object} configJSON - The object with keys and values to override.
|
|
* @returns {Object} - The result object only with the keys
|
|
* that are whitelisted.
|
|
*/
|
|
export function getWhitelistedJSON(configName: 'interfaceConfig' | 'config', configJSON: any): Object {
|
|
// Disable whitelisting in dev mode.
|
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
logger.warn('Whitelisting is disabled in dev mode, accepting any overrides');
|
|
|
|
return configJSON;
|
|
}
|
|
|
|
if (configName === 'interfaceConfig') {
|
|
return pick(configJSON, INTERFACE_CONFIG_WHITELIST);
|
|
} else if (configName === 'config') {
|
|
return pick(configJSON, CONFIG_WHITELIST);
|
|
}
|
|
|
|
return configJSON;
|
|
}
|
|
|
|
/**
|
|
* Selector for determining if the display name is read only.
|
|
*
|
|
* @param {Object} state - The state of the app.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isNameReadOnly(state: IReduxState): boolean {
|
|
return Boolean(state['features/base/config'].disableProfile
|
|
|| state['features/base/config'].readOnlyName);
|
|
}
|
|
|
|
/**
|
|
* Selector for determining if the participant is the next one in the queue to speak.
|
|
*
|
|
* @param {Object} state - The state of the app.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isNextToSpeak(state: IReduxState): boolean {
|
|
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue || [];
|
|
const participantId = getLocalParticipant(state)?.id;
|
|
|
|
return participantId === raisedHandsQueue[0]?.id;
|
|
}
|
|
|
|
/**
|
|
* Selector for determining if the next to speak participant in the queue has been notified.
|
|
*
|
|
* @param {Object} state - The state of the app.
|
|
* @returns {boolean}
|
|
*/
|
|
export function hasBeenNotified(state: IReduxState): boolean {
|
|
const raisedHandsQueue = state['features/base/participants'].raisedHandsQueue;
|
|
|
|
return Boolean(raisedHandsQueue[0]?.hasBeenNotified);
|
|
}
|
|
|
|
/**
|
|
* Selector for determining if the display name is visible.
|
|
*
|
|
* @param {Object} state - The state of the app.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isDisplayNameVisible(state: IReduxState): boolean {
|
|
return !state['features/base/config'].hideDisplayName;
|
|
}
|
|
|
|
/**
|
|
* Restores a Jitsi Meet config.js from {@code localStorage} if it was
|
|
* previously downloaded from a specific {@code baseURL} and stored with
|
|
* {@link storeConfig}.
|
|
*
|
|
* @param {string} baseURL - The base URL from which the config.js was
|
|
* previously downloaded and stored with {@code storeConfig}.
|
|
* @returns {?Object} The Jitsi Meet config.js which was previously downloaded
|
|
* from {@code baseURL} and stored with {@code storeConfig} if it was restored;
|
|
* otherwise, {@code undefined}.
|
|
*/
|
|
export function restoreConfig(baseURL: string) {
|
|
const key = `${_CONFIG_STORE_PREFIX}/${baseURL}`;
|
|
const config = jitsiLocalStorage.getItem(key);
|
|
|
|
if (config) {
|
|
try {
|
|
return safeJsonParse(config) || undefined;
|
|
} catch (e) {
|
|
// Somehow incorrect data ended up in the storage. Clean it up.
|
|
jitsiLocalStorage.removeItem(key);
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Inspects the hash part of the location URI and overrides values specified
|
|
* there in the corresponding config objects given as the arguments. The syntax
|
|
* is: {@code https://server.com/room#config.debug=true
|
|
* &interfaceConfig.showButton=false}.
|
|
*
|
|
* In the hash part each parameter will be parsed to JSON and then the root
|
|
* object will be matched with the corresponding config object given as the
|
|
* argument to this function.
|
|
*
|
|
* @param {Object} config - This is the general config.
|
|
* @param {Object} interfaceConfig - This is the interface config.
|
|
* @param {URI} location - The new location to which the app is navigating to.
|
|
* @returns {void}
|
|
*/
|
|
export function setConfigFromURLParams(
|
|
config: IConfig, interfaceConfig: any, location: string | URL) {
|
|
const params = parseURLParams(location);
|
|
const json: any = {};
|
|
|
|
// At this point we have:
|
|
// params = {
|
|
// "config.disableAudioLevels": false,
|
|
// "config.channelLastN": -1,
|
|
// "interfaceConfig.APP_NAME": "Jitsi Meet"
|
|
// }
|
|
// We want to have:
|
|
// json = {
|
|
// config: {
|
|
// "disableAudioLevels": false,
|
|
// "channelLastN": -1
|
|
// },
|
|
// interfaceConfig: {
|
|
// "APP_NAME": "Jitsi Meet"
|
|
// }
|
|
// }
|
|
config && (json.config = {});
|
|
interfaceConfig && (json.interfaceConfig = {});
|
|
|
|
for (const param of Object.keys(params)) {
|
|
let base = json;
|
|
const names = param.split('.');
|
|
const last = names.pop() ?? '';
|
|
|
|
for (const name of names) {
|
|
base = base[name] = base[name] || {};
|
|
}
|
|
|
|
base[last] = params[param];
|
|
}
|
|
|
|
overrideConfigJSON(config, interfaceConfig, json);
|
|
|
|
// Print warning about deprecated URL params
|
|
if ('interfaceConfig.SUPPORT_URL' in params) {
|
|
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
|
|
+ ' Please use supportUrl from advanced branding!');
|
|
}
|
|
|
|
if ('config.defaultLogoUrl' in params) {
|
|
logger.warn('Using defaultLogoUrl config URL overwrite is deprecated.'
|
|
+ ' Please use logoImageUrl from advanced branding!');
|
|
}
|
|
|
|
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
|
|
|
if ('config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|
|
|| (typeof deploymentUrlsConfig === 'object'
|
|
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig))) {
|
|
logger.warn('Using deploymentUrls config URL overwrite is deprecated.'
|
|
+ ' Please use downloadAppsUrl and/or userDocumentationURL from advanced branding!');
|
|
}
|
|
|
|
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
|
|
|
|
if (('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|
|
|| ('config.liveStreaming.termsLink' in params)
|
|
|| ('config.liveStreaming.dataPrivacyLink' in params)
|
|
|| ('config.liveStreaming.helpLink' in params)
|
|
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
|
|
&& (
|
|
'termsLink' in liveStreamingConfig
|
|
|| 'dataPrivacyLink' in liveStreamingConfig
|
|
|| 'helpLink' in liveStreamingConfig
|
|
)
|
|
)) {
|
|
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
|
|
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
|
|
}
|
|
|
|
// When not in an iframe, start without media if the pre-join page is not enabled.
|
|
if (!isEmbedded()
|
|
&& ('config.prejoinConfig.enabled' in params || 'config.prejoinPageEnabled' in params)
|
|
&& (config.prejoinConfig?.enabled === false || config.prejoinPageEnabled === false)) {
|
|
logger.warn('Using prejoinConfig.enabled config URL overwrite implies starting without media.');
|
|
config.disableInitialGUM = true;
|
|
}
|
|
}
|
|
|
|
/* eslint-enable max-params */
|
|
|
|
/**
|
|
* Returns the dial out url.
|
|
*
|
|
* @param {Object} state - The state of the app.
|
|
* @returns {string}
|
|
*/
|
|
export function getDialOutStatusUrl(state: IReduxState) {
|
|
return state['features/base/config'].guestDialOutStatusUrl;
|
|
}
|
|
|
|
/**
|
|
* Returns the dial out status url.
|
|
*
|
|
* @param {Object} state - The state of the app.
|
|
* @returns {string}
|
|
*/
|
|
export function getDialOutUrl(state: IReduxState) {
|
|
return state['features/base/config'].guestDialOutUrl;
|
|
}
|
|
|
|
/**
|
|
* Selector to return the security UI config.
|
|
*
|
|
* @param {IReduxState} state - State object.
|
|
* @returns {Object}
|
|
*/
|
|
export function getSecurityUiConfig(state: IReduxState) {
|
|
return state['features/base/config']?.securityUi || {};
|
|
}
|
|
|
|
/**
|
|
* Returns the terms, privacy and help centre URL's.
|
|
*
|
|
* @param {IReduxState} state - The state of the application.
|
|
* @returns {{
|
|
* privacy: string,
|
|
* helpCentre: string,
|
|
* terms: string
|
|
* }}
|
|
*/
|
|
export function getLegalUrls(state: IReduxState) {
|
|
const helpCentreURL = state['features/base/config']?.helpCentreURL;
|
|
const configLegalUrls = state['features/base/config']?.legalUrls;
|
|
|
|
return {
|
|
privacy: configLegalUrls?.privacy || DEFAULT_PRIVACY_URL,
|
|
helpCentre: helpCentreURL || configLegalUrls?.helpCentre || DEFAULT_HELP_CENTRE_URL,
|
|
terms: configLegalUrls?.terms || DEFAULT_TERMS_URL
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Utility function to debounce the execution of a callback function.
|
|
*
|
|
* @param {Function} callback - The callback to debounce.
|
|
* @param {number} delay - The debounce delay in milliseconds.
|
|
* @returns {Function} - A debounced function that delays the execution of the callback.
|
|
*/
|
|
export function debounce(callback: (...args: any[]) => void, delay: number) {
|
|
let timerId: any;
|
|
|
|
return (...args: any[]) => {
|
|
if (timerId) {
|
|
clearTimeout(timerId);
|
|
}
|
|
timerId = setTimeout(() => callback(...args), delay);
|
|
};
|
|
}
|