/* global __dirname */ const { BrowserWindow, Menu, app, ipcMain } = require('electron'); const contextMenu = require('electron-context-menu'); const debug = require('electron-debug'); const isDev = require('electron-is-dev'); const { autoUpdater } = require('electron-updater'); const windowStateKeeper = require('electron-window-state'); const { initPopupsConfigurationMain, getPopupTarget, RemoteControlMain, setupAlwaysOnTopMain, setupPowerMonitorMain, setupScreenSharingMain } = require('@jitsi/electron-sdk'); const path = require('path'); const process = require('process'); const URL = require('url'); const config = require('./app/features/config'); const { openExternalLink } = require('./app/features/utils/openExternalLink'); const pkgJson = require('./package.json'); const showDevTools = Boolean(process.env.SHOW_DEV_TOOLS) || (process.argv.indexOf('--show-dev-tools') > -1); const ENABLE_REMOTE_CONTROL = false; // We need this because of https://github.com/electron/electron/issues/18214 app.commandLine.appendSwitch('disable-site-isolation-trials'); // This allows BrowserWindow.setContentProtection(true) to work on macOS. // https://github.com/electron/electron/issues/19880 app.commandLine.appendSwitch('disable-features', 'DesktopCaptureMacV2,IOSurfaceCapturer'); // Enable Opus RED field trial. app.commandLine.appendSwitch('force-fieldtrials', 'WebRTC-Audio-Red-For-Opus/Enabled/'); // Wayland: Enable optional PipeWire support. if (!app.commandLine.hasSwitch('enable-features')) { app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer'); } autoUpdater.logger = require('electron-log'); autoUpdater.logger.transports.file.level = 'info'; // Enable context menu so things like copy and paste work in input fields. contextMenu({ showLookUpSelection: false, showSearchWithGoogle: false, showCopyImage: false, showCopyImageAddress: false, showSaveImage: false, showSaveImageAs: false, showInspectElement: true, showServices: false }); // Enable DevTools also on release builds to help troubleshoot issues. Don't // show them automatically though. debug({ isEnabled: true, showDevTools }); /** * When in development mode: * - Enable automatic reloads */ if (isDev) { require('electron-reload')(path.join(__dirname, 'build')); } /** * The window object that will load the iframe with Jitsi Meet. * IMPORTANT: Must be defined as global in order to not be garbage collected * acidentally. */ let mainWindow = null; let webrtcInternalsWindow = null; /** * Add protocol data */ const appProtocolSurplus = `${config.default.appProtocolPrefix}://`; let rendererReady = false; let protocolDataForFrontApp = null; /** * Sets the application menu. It is hidden on all platforms except macOS because * otherwise copy and paste functionality is not available. */ function setApplicationMenu() { if (process.platform === 'darwin') { const template = [ { label: app.name, submenu: [ { role: 'services', submenu: [] }, { type: 'separator' }, { role: 'hide' }, { role: 'hideothers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] }, { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' } ] }, { label: '&Window', role: 'window', submenu: [ { role: 'minimize' }, { role: 'close' } ] } ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } else { Menu.setApplicationMenu(null); } } /** * Opens new window with index.html(Jitsi Meet is loaded in iframe there). */ function createJitsiMeetWindow() { // Application menu. setApplicationMenu(); // Check for Updates. if (!process.mas) { autoUpdater.checkForUpdatesAndNotify(); } // Load the previous window state with fallback to defaults. const windowState = windowStateKeeper({ defaultWidth: 800, defaultHeight: 600, fullScreen: false }); // Path to root directory. const basePath = isDev ? __dirname : app.getAppPath(); // URL for index.html which will be our entry point. const indexURL = URL.format({ pathname: path.resolve(basePath, './build/index.html'), protocol: 'file:', slashes: true }); // Options used when creating the main Jitsi Meet window. // Use a preload script in order to provide node specific functionality // to a isolated BrowserWindow in accordance with electron security // guideline. const options = { x: windowState.x, y: windowState.y, width: windowState.width, height: windowState.height, icon: path.resolve(basePath, './resources/icon.png'), minWidth: 800, minHeight: 600, show: false, webPreferences: { enableBlinkFeatures: 'WebAssemblyCSP', contextIsolation: false, nodeIntegration: false, preload: path.resolve(basePath, './build/preload.js'), sandbox: false } }; const windowOpenHandler = ({ url, frameName }) => { const target = getPopupTarget(url, frameName); if (!target || target === 'browser') { openExternalLink(url); return { action: 'deny' }; } if (target === 'electron') { return { action: 'allow' }; } return { action: 'deny' }; }; mainWindow = new BrowserWindow(options); windowState.manage(mainWindow); mainWindow.loadURL(indexURL); mainWindow.webContents.setWindowOpenHandler(windowOpenHandler); if (isDev) { mainWindow.webContents.session.clearCache(); } // Block access to file:// URLs. const fileFilter = { urls: [ 'file://*' ] }; mainWindow.webContents.session.webRequest.onBeforeSendHeaders(fileFilter, (details, callback) => { const requestedPath = path.resolve(URL.fileURLToPath(details.url)); const appBasePath = path.resolve(basePath); if (!requestedPath.startsWith(appBasePath)) { callback({ cancel: true }); console.warn(`Rejected file URL: ${details.url}`); return; } callback({ cancel: false }); }); // Filter out x-frame-options and frame-ancestors CSP to allow loading jitsi via the iframe API // Resolves https://github.com/jitsi/jitsi-meet-electron/issues/285 mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => { delete details.responseHeaders['x-frame-options']; if (details.responseHeaders['content-security-policy']) { const cspFiltered = details.responseHeaders['content-security-policy'][0] .split(';') .filter(x => x.indexOf('frame-ancestors') === -1) .join(';'); details.responseHeaders['content-security-policy'] = [ cspFiltered ]; } callback({ responseHeaders: details.responseHeaders }); }); // Block redirects. const allowedRedirects = [ 'http:', 'https:', 'ws:', 'wss:' ]; mainWindow.webContents.addListener('will-redirect', (ev, url) => { const requestedUrl = new URL.URL(url); if (!allowedRedirects.includes(requestedUrl.protocol)) { console.warn(`Disallowing redirect to ${url}`); ev.preventDefault(); } }); // Block opening any external applications. mainWindow.webContents.session.setPermissionRequestHandler((_, permission, callback, details) => { if (permission === 'openExternal') { console.warn(`Disallowing opening ${details.externalURL}`); callback(false); return; } callback(true); }); initPopupsConfigurationMain(mainWindow); setupAlwaysOnTopMain(mainWindow, null, windowOpenHandler); setupPowerMonitorMain(mainWindow); setupScreenSharingMain(mainWindow, config.default.appName, pkgJson.build.appId); if (ENABLE_REMOTE_CONTROL) { new RemoteControlMain(mainWindow); // eslint-disable-line no-new } mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.once('ready-to-show', () => { mainWindow.show(); }); /** * When someone tries to enter something like jitsi-meet://test * while app is closed * it will trigger this event below */ handleProtocolCall(process.argv.pop()); } /** * Opens new window with WebRTC internals. */ function createWebRTCInternalsWindow() { const options = { minWidth: 800, minHeight: 600, show: true }; webrtcInternalsWindow = new BrowserWindow(options); webrtcInternalsWindow.loadURL('chrome://webrtc-internals'); } /** * Handler for application protocol links to initiate a conference. */ function handleProtocolCall(fullProtocolCall) { // don't touch when something is bad if ( !fullProtocolCall || fullProtocolCall.trim() === '' || fullProtocolCall.indexOf(appProtocolSurplus) !== 0 ) { return; } const inputURL = fullProtocolCall.replace(appProtocolSurplus, ''); if (app.isReady() && mainWindow === null) { createJitsiMeetWindow(); } protocolDataForFrontApp = inputURL; if (rendererReady) { mainWindow .webContents .send('protocol-data-msg', inputURL); } } /** * Force Single Instance Application. * Handle this on darwin via LSMultipleInstancesProhibited in Info.plist as below does not work on MAS */ const gotInstanceLock = process.platform === 'darwin' ? true : app.requestSingleInstanceLock(); if (!gotInstanceLock) { app.quit(); process.exit(0); } /** * Run the application. */ app.on('activate', () => { if (mainWindow === null) { createJitsiMeetWindow(); } }); app.on('certificate-error', // eslint-disable-next-line max-params (event, webContents, url, error, certificate, callback) => { if (isDev) { event.preventDefault(); callback(true); } else { callback(false); } } ); app.on('ready', createJitsiMeetWindow); if (isDev) { app.on('ready', createWebRTCInternalsWindow); } app.on('second-instance', (event, commandLine) => { /** * If someone creates second instance of the application, set focus on * existing window. */ if (mainWindow) { mainWindow.isMinimized() && mainWindow.restore(); mainWindow.focus(); /** * This is for windows [win32] * so when someone tries to enter something like jitsi-meet://test * while app is opened it will trigger protocol handler. */ handleProtocolCall(commandLine.pop()); } }); app.on('window-all-closed', () => { app.quit(); }); // remove so we can register each time as we run the app. app.removeAsDefaultProtocolClient(config.default.appProtocolPrefix); // If we are running a non-packaged version of the app && on windows if (isDev && process.platform === 'win32') { // Set the path of electron.exe and your app. // These two additional parameters are only available on windows. app.setAsDefaultProtocolClient( config.default.appProtocolPrefix, process.execPath, [ path.resolve(process.argv[1]) ] ); } else { app.setAsDefaultProtocolClient(config.default.appProtocolPrefix); } /** * This is for mac [darwin] * so when someone tries to enter something like jitsi-meet://test * it will trigger this event below */ app.on('open-url', (event, data) => { event.preventDefault(); handleProtocolCall(data); }); /** * This is to notify main.js [this] that front app is ready to receive messages. */ ipcMain.on('renderer-ready', () => { rendererReady = true; if (protocolDataForFrontApp) { mainWindow .webContents .send('protocol-data-msg', protocolDataForFrontApp); } }); /** * Handle opening external links in the main process. */ ipcMain.on('jitsi-open-url', (event, someUrl) => { openExternalLink(someUrl); });