aboutsummaryrefslogtreecommitdiffstats
path: root/src/index.js
diff options
context:
space:
mode:
authorLibravatar Markus Hatvan <markus_hatvan@aon.at>2021-10-10 10:42:20 +0200
committerLibravatar GitHub <noreply@github.com>2021-10-10 10:42:20 +0200
commita673c2b577a31437c91a0b498e69b0753d62aa58 (patch)
tree50d52df3ff9d78828ca06a24ef438e31b062ca56 /src/index.js
parentremove unused 'conventional-changelog' npm package. Change the url for the ch... (diff)
downloadferdium-app-a673c2b577a31437c91a0b498e69b0753d62aa58.tar.gz
ferdium-app-a673c2b577a31437c91a0b498e69b0753d62aa58.tar.zst
ferdium-app-a673c2b577a31437c91a0b498e69b0753d62aa58.zip
chore: convert index file to TS (#2049)
Diffstat (limited to 'src/index.js')
-rw-r--r--src/index.js635
1 files changed, 0 insertions, 635 deletions
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 933637688..000000000
--- a/src/index.js
+++ /dev/null
@@ -1,635 +0,0 @@
1/* eslint-disable import/first */
2
3import { app, BrowserWindow, globalShortcut, ipcMain, session, dialog } from 'electron';
4
5import { emptyDirSync, ensureFileSync } from 'fs-extra';
6import { join } from 'path';
7import windowStateKeeper from 'electron-window-state';
8import ms from 'ms';
9import { initializeRemote } from './electron-util';
10import { enforceMacOSAppLocation } from './enforce-macos-app-location';
11
12initializeRemote();
13
14import { DEFAULT_APP_SETTINGS, DEFAULT_WINDOW_OPTIONS } from './config';
15
16import {
17 isMac,
18 isWindows,
19 isLinux,
20 altKey,
21} from './environment';
22import {
23 isDevMode,
24 aboutAppDetails,
25 userDataRecipesPath,
26 userDataPath,
27} from './environment-remote';
28import { ifUndefinedBoolean } from './jsUtils';
29
30import { mainIpcHandler as basicAuthHandler } from './features/basicAuth';
31import ipcApi from './electron/ipc-api';
32import Tray from './lib/Tray';
33import DBus from './lib/DBus';
34import Settings from './electron/Settings';
35import handleDeepLink from './electron/deepLinking';
36import { isPositionValid } from './electron/windowUtils';
37import { appId } from './package.json'; // eslint-disable-line import/no-unresolved
38import './electron/exception';
39
40import { asarPath } from './helpers/asar-helpers';
41import { openExternalUrl } from './helpers/url-helpers';
42import userAgent from './helpers/userAgent-helpers';
43
44const debug = require('debug')('Ferdi:App');
45
46// Globally set useragent to fix user agent override in service workers
47debug('Set userAgent to ', userAgent());
48app.userAgentFallback = userAgent();
49
50// Keep a global reference of the window object, if you don't, the window will
51// be closed automatically when the JavaScript object is garbage collected.
52let mainWindow;
53let willQuitApp = false;
54
55// Register methods to be called once the window has been loaded.
56let onDidLoadFns = [];
57
58function onDidLoad(fn) {
59 if (onDidLoadFns) {
60 onDidLoadFns.push(fn);
61 } else if (mainWindow) {
62 fn(mainWindow);
63 }
64}
65
66// Ensure that the recipe directory exists
67emptyDirSync(userDataRecipesPath('temp'));
68ensureFileSync(userDataPath('window-state.json'));
69
70// Set App ID for Windows
71if (isWindows) {
72 app.setAppUserModelId(appId);
73}
74
75// Initialize Settings
76const settings = new Settings('app', DEFAULT_APP_SETTINGS);
77const proxySettings = new Settings('proxy');
78
79const retrieveSettingValue = (key, defaultValue) =>
80 ifUndefinedBoolean(settings.get(key), defaultValue);
81
82if (retrieveSettingValue('sentry', DEFAULT_APP_SETTINGS.sentry)) {
83 // eslint-disable-next-line global-require
84 require('./sentry');
85}
86
87const liftSingleInstanceLock = retrieveSettingValue(
88 'liftSingleInstanceLock',
89 false,
90);
91
92// Force single window
93const gotTheLock = liftSingleInstanceLock
94 ? true
95 : app.requestSingleInstanceLock();
96if (!gotTheLock) {
97 app.quit();
98} else {
99 app.on('second-instance', (event, argv) => {
100 // Someone tried to run a second instance, we should focus our window.
101 if (mainWindow) {
102 if (!mainWindow.isVisible()) {
103 mainWindow.show();
104 }
105 if (mainWindow.isMinimized()) {
106 mainWindow.restore();
107 }
108 mainWindow.focus();
109
110 if (isWindows) {
111 onDidLoad(window => {
112 // Keep only command line / deep linked arguments
113 const url = argv.slice(1);
114 if (url) {
115 handleDeepLink(window, url.toString());
116 }
117
118 if (argv.includes('--reset-window')) {
119 // Needs to be delayed to not interfere with mainWindow.restore();
120 setTimeout(() => {
121 debug('Resetting windows via Task');
122 window.setPosition(
123 DEFAULT_WINDOW_OPTIONS.x + 100,
124 DEFAULT_WINDOW_OPTIONS.y + 100,
125 );
126 window.setSize(
127 DEFAULT_WINDOW_OPTIONS.width,
128 DEFAULT_WINDOW_OPTIONS.height,
129 );
130 }, 1);
131 } else if (argv.includes('--quit')) {
132 // Needs to be delayed to not interfere with mainWindow.restore();
133 setTimeout(() => {
134 debug('Quitting Ferdi via Task');
135 app.quit();
136 }, 1);
137 }
138 });
139 }
140 }
141 });
142}
143
144// Fix Unity indicator issue
145// https://github.com/electron/electron/issues/9046
146if (
147 isLinux &&
148 ['Pantheon', 'Unity:Unity7'].includes(process.env.XDG_CURRENT_DESKTOP)
149) {
150 process.env.XDG_CURRENT_DESKTOP = 'Unity';
151}
152
153// Disable GPU acceleration
154if (!retrieveSettingValue('enableGPUAcceleration', false)) {
155 debug('Disable GPU Acceleration');
156 app.disableHardwareAcceleration();
157}
158
159app.setAboutPanelOptions({
160 applicationVersion: aboutAppDetails(),
161 version: '',
162});
163
164const createWindow = () => {
165 // Remember window size
166 const mainWindowState = windowStateKeeper({
167 defaultWidth: DEFAULT_WINDOW_OPTIONS.width,
168 defaultHeight: DEFAULT_WINDOW_OPTIONS.height,
169 maximize: true, // Automatically maximizes the window, if it was last closed maximized
170 fullScreen: true, // Automatically restores the window to full screen, if it was last closed full screen
171 });
172
173 let posX = mainWindowState.x || DEFAULT_WINDOW_OPTIONS.x;
174 let posY = mainWindowState.y || DEFAULT_WINDOW_OPTIONS.y;
175
176 if (!isPositionValid({ x: posX, y: posY })) {
177 debug('Window is out of screen bounds, resetting window');
178 posX = DEFAULT_WINDOW_OPTIONS.x;
179 posY = DEFAULT_WINDOW_OPTIONS.y;
180 }
181
182 // Create the browser window.
183 const backgroundColor = retrieveSettingValue('darkMode', false)
184 ? '#1E1E1E'
185 : settings.get('accentColor');
186
187 mainWindow = new BrowserWindow({
188 x: posX,
189 y: posY,
190 width: mainWindowState.width,
191 height: mainWindowState.height,
192 minWidth: 600,
193 minHeight: 500,
194 show: false,
195 titleBarStyle: isMac ? 'hidden' : 'default',
196 frame: isLinux,
197 backgroundColor,
198 webPreferences: {
199 spellcheck: retrieveSettingValue('enableSpellchecking', DEFAULT_APP_SETTINGS.enableSpellchecking),
200 nodeIntegration: true,
201 contextIsolation: false,
202 webviewTag: true,
203 preload: join(__dirname, 'sentry.js'),
204 enableRemoteModule: true,
205 },
206 });
207
208 app.on('web-contents-created', (e, contents) => {
209 if (contents.getType() === 'webview') {
210 contents.on('new-window', event => {
211 event.preventDefault();
212 });
213 }
214 });
215
216 mainWindow.webContents.on('did-finish-load', () => {
217 const fns = onDidLoadFns;
218 onDidLoadFns = null;
219
220 if (!fns) return;
221
222 for (const fn of fns) {
223 fn(mainWindow);
224 }
225 });
226
227 // Initialize System Tray
228 const trayIcon = new Tray();
229
230 // Initialize DBus interface
231 const dbus = new DBus(trayIcon);
232
233 // Initialize ipcApi
234 ipcApi({
235 mainWindow,
236 settings: {
237 app: settings,
238 proxy: proxySettings,
239 },
240 trayIcon,
241 });
242
243 // Connect to the DBus after ipcApi took care of the System Tray
244 dbus.start();
245
246 // Manage Window State
247 mainWindowState.manage(mainWindow);
248
249 // and load the index.html of the app.
250 mainWindow.loadURL(`file://${__dirname}/index.html`);
251
252 // Open the DevTools.
253 if (isDevMode || process.argv.includes('--devtools')) {
254 mainWindow.webContents.openDevTools();
255 }
256
257 // Windows deep linking handling on app launch
258 if (isWindows) {
259 onDidLoad(window => {
260 const url = process.argv.slice(1);
261 if (url) {
262 handleDeepLink(window, url.toString());
263 }
264 });
265 }
266
267 // Emitted when the window is closed.
268 mainWindow.on('close', e => {
269 debug('Window: close window');
270 // Dereference the window object, usually you would store windows
271 // in an array if your app supports multi windows, this is the time
272 // when you should delete the corresponding element.
273 if (!willQuitApp && retrieveSettingValue('runInBackground', DEFAULT_APP_SETTINGS.runInBackground)) {
274 e.preventDefault();
275 if (isWindows) {
276 debug('Window: minimize');
277 mainWindow.minimize();
278
279 if (retrieveSettingValue('closeToSystemTray', DEFAULT_APP_SETTINGS.closeToSystemTray)) {
280 debug('Skip taskbar: true');
281 mainWindow.setSkipTaskbar(true);
282 }
283 } else if (isMac && mainWindow.isFullScreen()) {
284 debug('Window: leaveFullScreen and hide');
285 mainWindow.once('show', () => mainWindow.setFullScreen(true));
286 mainWindow.once('leave-full-screen', () => mainWindow.hide());
287 mainWindow.setFullScreen(false);
288 } else {
289 debug('Window: hide');
290 mainWindow.hide();
291 }
292 } else {
293 dbus.stop();
294 app.quit();
295 }
296 });
297
298 // For Windows we need to store a flag to properly restore the window
299 // if the window was maximized before minimizing it so system tray
300 mainWindow.on('minimize', () => {
301 app.wasMaximized = app.isMaximized;
302
303 if (retrieveSettingValue('minimizeToSystemTray', DEFAULT_APP_SETTINGS.minimizeToSystemTray)) {
304 debug('Skip taskbar: true');
305 mainWindow.setSkipTaskbar(true);
306 trayIcon.show();
307 }
308 });
309
310 mainWindow.on('maximize', () => {
311 debug('Window: maximize');
312 app.isMaximized = true;
313 });
314
315 mainWindow.on('unmaximize', () => {
316 debug('Window: unmaximize');
317 app.isMaximized = false;
318 });
319
320 mainWindow.on('restore', () => {
321 debug('Window: restore');
322 mainWindow.setSkipTaskbar(false);
323
324 if (app.wasMaximized) {
325 debug('Window: was maximized before, maximize window');
326 mainWindow.maximize();
327 }
328
329 if (!retrieveSettingValue('enableSystemTray', DEFAULT_APP_SETTINGS.enableSystemTray)) {
330 debug('Tray: hiding tray icon');
331 trayIcon.hide();
332 }
333 });
334
335 if (isMac) {
336 // eslint-disable-next-line global-require
337 const { askFormacOSPermissions } = require('./electron/macOSPermissions');
338 setTimeout(() => askFormacOSPermissions(mainWindow), ms('30s'));
339 }
340
341 mainWindow.on('show', () => {
342 debug('Skip taskbar: true');
343 mainWindow.setSkipTaskbar(false);
344 });
345
346 app.mainWindow = mainWindow;
347 app.isMaximized = mainWindow.isMaximized();
348
349 mainWindow.webContents.on('new-window', (e, url) => {
350 e.preventDefault();
351 openExternalUrl(url);
352 });
353
354 if (retrieveSettingValue('startMinimized', DEFAULT_APP_SETTINGS.startMinimized)) {
355 mainWindow.hide();
356 } else {
357 mainWindow.show();
358 }
359
360 app.whenReady().then(() => {
361 if (retrieveSettingValue('enableGlobalHideShortcut', DEFAULT_APP_SETTINGS.enableGlobalHideShortcut)) {
362 // Toggle the window on 'Alt+X'
363 globalShortcut.register(`${altKey()}+X`, () => {
364 trayIcon.trayMenuTemplate[0].click();
365 });
366 }
367 });
368};
369
370// Allow passing command line parameters/switches to electron
371// https://electronjs.org/docs/api/chrome-command-line-switches
372// used for Kerberos support
373// Usage e.g. MACOS
374// $ Ferdi.app/Contents/MacOS/Ferdi --auth-server-whitelist *.mydomain.com --auth-negotiate-delegate-whitelist *.mydomain.com
375const argv = require('minimist')(process.argv.slice(1));
376
377if (argv['auth-server-whitelist']) {
378 app.commandLine.appendSwitch(
379 'auth-server-whitelist',
380 argv['auth-server-whitelist'],
381 );
382}
383if (argv['auth-negotiate-delegate-whitelist']) {
384 app.commandLine.appendSwitch(
385 'auth-negotiate-delegate-whitelist',
386 argv['auth-negotiate-delegate-whitelist'],
387 );
388}
389
390// Disable Chromium's poor MPRIS implementation
391// and apply workaround for https://github.com/electron/electron/pull/26432
392app.commandLine.appendSwitch(
393 'disable-features',
394 'HardwareMediaKeyHandling,MediaSessionService,CrossOriginOpenerPolicy',
395);
396
397// This method will be called when Electron has finished
398// initialization and is ready to create browser windows.
399// Some APIs can only be used after this event occurs.
400app.on('ready', () => {
401 // force app to live in /Applications
402 enforceMacOSAppLocation();
403
404 // Register App URL
405 const protocolClient = isDevMode ? 'ferdi-dev' : 'ferdi';
406 if (!app.isDefaultProtocolClient(protocolClient)) {
407 app.setAsDefaultProtocolClient(protocolClient);
408 }
409
410 if (isWindows) {
411 const extraArgs = isDevMode ? `${__dirname} ` : '';
412 const iconPath = asarPath(
413 join(
414 isDevMode ? `${__dirname}../src/` : __dirname,
415 'assets/images/taskbar/win32/display.ico',
416 ),
417 );
418 app.setUserTasks([
419 {
420 program: process.execPath,
421 arguments: `${extraArgs}--reset-window`,
422 iconPath,
423 iconIndex: 0,
424 title: 'Move Ferdi to Current Display',
425 description: 'Restore the position and size of Ferdi',
426 },
427 {
428 program: process.execPath,
429 arguments: `${extraArgs}--quit`,
430 iconPath,
431 iconIndex: 0,
432 title: 'Quit Ferdi',
433 description: null,
434 },
435 ]);
436 }
437
438 // eslint-disable-next-line global-require
439 require('electron-react-titlebar/main').initialize();
440
441 createWindow();
442});
443
444// This is the worst possible implementation as the webview.webContents based callback doesn't work 🖕
445// TODO: rewrite to handle multiple login calls
446const noop = () => null;
447let authCallback = noop;
448
449app.on('login', (event, webContents, request, authInfo, callback) => {
450 authCallback = callback;
451 debug('browser login event', authInfo);
452 event.preventDefault();
453
454 if (!authInfo.isProxy && authInfo.scheme === 'basic') {
455 debug('basic auth handler', authInfo);
456 basicAuthHandler(mainWindow, authInfo);
457 }
458});
459
460// TODO: evaluate if we need to store the authCallback for every service
461ipcMain.on('feature-basic-auth-credentials', (e, { user, password }) => {
462 debug('Received basic auth credentials', user, '********');
463
464 authCallback(user, password);
465 authCallback = noop;
466});
467
468ipcMain.on('open-browser-window', (e, { url, serviceId }) => {
469 const serviceSession = session.fromPartition(`persist:service-${serviceId}`);
470 const child = new BrowserWindow({
471 parent: mainWindow,
472 webPreferences: {
473 session: serviceSession,
474 // TODO: Aren't these needed here?
475 // contextIsolation: false,
476 // enableRemoteModule: true,
477 },
478 });
479 child.show();
480 child.loadURL(url);
481 debug('Received open-browser-window', url);
482});
483
484ipcMain.on(
485 'modifyRequestHeaders',
486 (e, { modifiedRequestHeaders, serviceId }) => {
487 debug(
488 `Received modifyRequestHeaders ${modifiedRequestHeaders} for serviceId ${serviceId}`,
489 );
490 for (const headerFilterSet of modifiedRequestHeaders) {
491 const { headers, requestFilters } = headerFilterSet;
492 session
493 .fromPartition(`persist:service-${serviceId}`)
494 .webRequest.onBeforeSendHeaders(requestFilters, (details, callback) => {
495 for (const key in headers) {
496 if (Object.prototype.hasOwnProperty.call(headers, key)) {
497 const value = headers[key];
498 details.requestHeaders[key] = value;
499 }
500 }
501 callback({ requestHeaders: details.requestHeaders });
502 });
503 }
504 },
505);
506
507ipcMain.on('knownCertificateHosts', (e, { knownHosts, serviceId }) => {
508 debug(
509 `Received knownCertificateHosts ${knownHosts} for serviceId ${serviceId}`,
510 );
511 session
512 .fromPartition(`persist:service-${serviceId}`)
513 .setCertificateVerifyProc((request, callback) => {
514 // To know more about these callbacks: https://www.electronjs.org/docs/api/session#sessetcertificateverifyprocproc
515 const { hostname } = request;
516 if (knownHosts.find(item => item.includes(hostname)).length > 0) {
517 callback(0);
518 } else {
519 callback(-2);
520 }
521 });
522});
523
524ipcMain.on('feature-basic-auth-cancel', () => {
525 debug('Cancel basic auth');
526
527 authCallback(null);
528 authCallback = noop;
529});
530
531// Handle synchronous messages from service webviews.
532
533ipcMain.on('find-in-page', (e, text, options) => {
534 const { sender: webContents } = e;
535 if (webContents !== mainWindow.webContents && typeof text === 'string') {
536 const sanitizedOptions = {};
537 for (const option of ['forward', 'findNext', 'matchCase']) {
538 if (option in options) {
539 sanitizedOptions[option] = !!options[option];
540 }
541 }
542 const requestId = webContents.findInPage(text, sanitizedOptions);
543 debug('Find in page', text, options, requestId);
544 e.returnValue = requestId;
545 } else {
546 e.returnValue = null;
547 }
548});
549
550ipcMain.on('stop-find-in-page', (e, action) => {
551 const { sender: webContents } = e;
552 if (webContents !== mainWindow.webContents) {
553 const validActions = [
554 'clearSelection',
555 'keepSelection',
556 'activateSelection',
557 ];
558 if (validActions.includes(action)) {
559 webContents.stopFindInPage(action);
560 }
561 }
562 e.returnValue = null;
563});
564
565ipcMain.on('set-spellchecker-locales', (e, { locale, serviceId }) => {
566 if (serviceId === undefined) {
567 return;
568 }
569
570 const serviceSession = session.fromPartition(`persist:service-${serviceId}`);
571 const [defaultLocale] = serviceSession.getSpellCheckerLanguages();
572 debug(`Spellchecker default locale is: ${defaultLocale}`);
573
574 const locales = [locale, defaultLocale, DEFAULT_APP_SETTINGS.fallbackLocale];
575 debug(`Setting spellchecker locales to: ${locales}`);
576 serviceSession.setSpellCheckerLanguages(locales);
577});
578
579// Quit when all windows are closed.
580app.on('window-all-closed', () => {
581 // On OS X it is common for applications and their menu bar
582 // to stay active until the user quits explicitly with Cmd + Q
583 if (retrieveSettingValue('runInBackground', DEFAULT_APP_SETTINGS.runInBackground)) {
584 debug('Window: all windows closed, quit app');
585 app.quit();
586 } else {
587 debug("Window: don't quit app");
588 }
589});
590
591app.on('before-quit', event => {
592 const yesButtonIndex = 0;
593 let selection = yesButtonIndex;
594 if (retrieveSettingValue('confirmOnQuit', DEFAULT_APP_SETTINGS.confirmOnQuit)) {
595 selection = dialog.showMessageBoxSync(app.mainWindow, {
596 type: 'question',
597 message: 'Quit',
598 detail: 'Do you really want to quit Ferdi?',
599 buttons: ['Yes', 'No'],
600 });
601 }
602 if (selection === yesButtonIndex) {
603 willQuitApp = true;
604 } else {
605 event.preventDefault();
606 }
607});
608
609app.on('activate', () => {
610 // On OS X it's common to re-create a window in the app when the
611 // dock icon is clicked and there are no other windows open.
612 if (mainWindow === null) {
613 createWindow();
614 } else {
615 mainWindow.show();
616 }
617});
618
619app.on('web-contents-created', (createdEvent, contents) => {
620 contents.on('new-window', (event, url, frameNme, disposition) => {
621 if (disposition === 'foreground-tab') event.preventDefault();
622 });
623});
624
625app.on('will-finish-launching', () => {
626 // Protocol handler for macOS
627 app.on('open-url', (event, url) => {
628 event.preventDefault();
629
630 onDidLoad(window => {
631 debug('open-url event', url);
632 handleDeepLink(window, url);
633 });
634 });
635});