const electron = require('electron'); const path = require('path'); const fs = require('fs'); const app = electron.app; let mainWindow = null; let appIcon = null; let ready = false; let appReady = false; let restartPending = false; let mainWindowPosition = {}; let updateMainWindowPositionTimeout = null; const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } let openFile = process.argv.filter(arg => /\.kdbx$/i.test(arg))[0]; const userDataDir = app.getPath('userData').replace(/[\\/]temp[\\/]\d+\.\d+[\\/]?$/, ''); const windowPositionFileName = path.join(userDataDir, 'window-position.json'); const appSettingsFileName = path.join(userDataDir, 'app-settings.json'); const tempUserDataPath = path.join(userDataDir, 'temp'); const tempUserDataPathRand = Date.now().toString() + Math.random().toString(); const systemNotificationIds = []; let htmlPath = process.argv .filter(arg => arg.startsWith('--htmlpath=')) .map(arg => arg.replace('--htmlpath=', ''))[0]; if (!htmlPath) { htmlPath = 'file://' + path.join(__dirname, 'index.html'); } const showDevToolsOnStart = process.argv.some(arg => arg.startsWith('--devtools')); app.setPath('userData', path.join(tempUserDataPath, tempUserDataPathRand)); setEnv(); restorePreferences(); app.on('window-all-closed', () => { if (restartPending) { // unbind all handlers, load new app.js module and pass control to it app.removeAllListeners('window-all-closed'); app.removeAllListeners('ready'); app.removeAllListeners('open-file'); app.removeAllListeners('activate'); app.removeAllListeners('second-instance'); electron.globalShortcut.unregisterAll(); electron.powerMonitor.removeAllListeners('suspend'); electron.powerMonitor.removeAllListeners('resume'); for (const id of systemNotificationIds) { electron.systemPreferences.unsubscribeNotification(id); } systemNotificationIds.length = 0; const userDataAppFile = path.join(userDataDir, 'app.asar/app.js'); delete require.cache[require.resolve('./app.js')]; require(userDataAppFile); app.emit('ready'); } else { if (process.platform !== 'darwin') { app.quit(); } } }); app.on('ready', () => { appReady = true; setAppOptions(); setSystemAppearance(); createMainWindow(); setGlobalShortcuts(); subscribePowerEvents(); deleteOldTempFiles(); hookRequestHeaders(); }); app.on('open-file', (e, path) => { e.preventDefault(); openFile = path; notifyOpenFile(); }); app.on('activate', () => { if (process.platform === 'darwin') { if (appReady && !mainWindow) { createMainWindow(); } } }); app.on('will-quit', () => { electron.globalShortcut.unregisterAll(); }); app.on('second-instance', () => { if (mainWindow) { restoreMainWindow(); } }); app.restartApp = function() { restartPending = true; mainWindow.close(); setTimeout(() => { restartPending = false; }, 1000); }; app.openWindow = function(opts) { return new electron.BrowserWindow(opts); }; app.minimizeApp = function() { let imagePath; mainWindow.hide(); if (process.platform === 'darwin') { app.dock.hide(); imagePath = 'mac-menubar-icon.png'; } else { imagePath = 'icon.png'; } mainWindow.setSkipTaskbar(true); if (!appIcon) { const image = electron.nativeImage.createFromPath(path.join(__dirname, imagePath)); appIcon = new electron.Tray(image); appIcon.on('click', restoreMainWindow); const contextMenu = electron.Menu.buildFromTemplate([ { label: 'Open KeeWeb', click: restoreMainWindow }, { label: 'Quit KeeWeb', click: closeMainWindow } ]); appIcon.setContextMenu(contextMenu); appIcon.setToolTip('KeeWeb'); } }; app.minimizeThenHideIfInTray = function() { // This function is called when auto-type has displayed a selection list and a selection was made. // To ensure focus returns to the previous window we must minimize first even if we're going to hide. mainWindow.minimize(); if (appIcon) mainWindow.hide(); }; app.getMainWindow = function() { return mainWindow; }; app.emitBackboneEvent = emitBackboneEvent; function setAppOptions() { app.commandLine.appendSwitch('disable-background-timer-throttling'); } function readAppSettings() { try { return JSON.parse(fs.readFileSync(appSettingsFileName, 'utf8')); } catch (e) { return null; } } function setSystemAppearance() { if (process.platform === 'darwin') { if (electron.systemPreferences.isDarkMode()) { electron.systemPreferences.setAppLevelAppearance('dark'); } } } function createMainWindow() { const appSettings = readAppSettings() || {}; const isMacDarkTheme = appSettings.theme === 'macdark'; const windowOptions = { show: false, width: 1000, height: 700, minWidth: 700, minHeight: 400, titleBarStyle: appSettings.titlebarStyle, backgroundColor: isMacDarkTheme ? '#1F1F20' : '#282C34', webPreferences: { backgroundThrottling: false, nodeIntegration: true, nodeIntegrationInWorker: true } }; if (process.platform !== 'win32') { windowOptions.icon = path.join(__dirname, 'icon.png'); } mainWindow = new electron.BrowserWindow(windowOptions); setMenu(); mainWindow.loadURL(htmlPath); if (showDevToolsOnStart) { mainWindow.openDevTools({ mode: 'bottom' }); } mainWindow.once('ready-to-show', () => { mainWindow.show(); ready = true; notifyOpenFile(); }); mainWindow.webContents.on('context-menu', onContextMenu); mainWindow.on('resize', delaySaveMainWindowPosition); mainWindow.on('move', delaySaveMainWindowPosition); mainWindow.on('restore', coerceMainWindowPositionToConnectedDisplay); mainWindow.on('close', updateMainWindowPositionIfPending); mainWindow.on('focus', mainWindowFocus); mainWindow.on('blur', mainWindowBlur); mainWindow.on('closed', () => { mainWindow = null; saveMainWindowPosition(); }); mainWindow.on('minimize', () => { emitBackboneEvent('launcher-minimize'); }); mainWindow.on('leave-full-screen', () => { emitBackboneEvent('leave-full-screen'); }); mainWindow.on('enter-full-screen', () => { emitBackboneEvent('enter-full-screen'); }); mainWindow.on('session-end', () => { emitBackboneEvent('os-lock'); }); restoreMainWindowPosition(); } function restoreMainWindow() { // if (process.platform === 'darwin') { // app.dock.show(); // mainWindow.show(); // } if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.setSkipTaskbar(false); mainWindow.show(); coerceMainWindowPositionToConnectedDisplay(); setTimeout(destroyAppIcon, 0); } function closeMainWindow() { emitBackboneEvent('launcher-exit-request'); setTimeout(destroyAppIcon, 0); } function destroyAppIcon() { if (appIcon) { appIcon.destroy(); appIcon = null; } } function delaySaveMainWindowPosition() { if (updateMainWindowPositionTimeout) { clearTimeout(updateMainWindowPositionTimeout); } updateMainWindowPositionTimeout = setTimeout(updateMainWindowPosition, 500); } function updateMainWindowPositionIfPending() { if (updateMainWindowPositionTimeout) { clearTimeout(updateMainWindowPositionTimeout); updateMainWindowPosition(); } } function updateMainWindowPosition() { if (!mainWindow) { return; } updateMainWindowPositionTimeout = null; const bounds = mainWindow.getBounds(); if (!mainWindow.isMaximized() && !mainWindow.isMinimized() && !mainWindow.isFullScreen()) { mainWindowPosition.x = bounds.x; mainWindowPosition.y = bounds.y; mainWindowPosition.width = bounds.width; mainWindowPosition.height = bounds.height; } mainWindowPosition.maximized = mainWindow.isMaximized(); mainWindowPosition.fullScreen = mainWindow.isFullScreen(); mainWindowPosition.changed = true; } function saveMainWindowPosition() { if (!mainWindowPosition.changed) { return; } delete mainWindowPosition.changed; try { fs.writeFileSync(windowPositionFileName, JSON.stringify(mainWindowPosition), 'utf8'); } catch (e) {} } function restoreMainWindowPosition() { fs.readFile(windowPositionFileName, 'utf8', (e, data) => { if (data) { mainWindowPosition = JSON.parse(data); if (mainWindow && mainWindowPosition) { if (mainWindowPosition.width && mainWindowPosition.height) { mainWindow.setBounds(mainWindowPosition); coerceMainWindowPositionToConnectedDisplay(); } if (mainWindowPosition.maximized) { mainWindow.maximize(); } if (mainWindowPosition.fullScreen) { mainWindow.setFullScreen(true); } } } }); } function mainWindowBlur() { emitBackboneEvent('main-window-blur'); } function mainWindowFocus() { emitBackboneEvent('main-window-focus'); } function emitBackboneEvent(e, arg) { if (mainWindow && mainWindow.webContents) { arg = JSON.stringify(arg); mainWindow.webContents.executeJavaScript(`Backbone.trigger('${e}', ${arg}); void 0;`); } } function setMenu() { if (process.platform === 'darwin') { const name = require('electron').app.getName(); const template = [ { label: name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services', submenu: [] }, { type: 'separator' }, { accelerator: 'Command+H', role: 'hide' }, { accelerator: 'Command+Shift+H', role: 'hideothers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit', accelerator: 'Command+Q' } ] }, { label: 'Edit', submenu: [ { accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, { type: 'separator' }, { accelerator: 'CmdOrCtrl+X', role: 'cut' }, { accelerator: 'CmdOrCtrl+C', role: 'copy' }, { accelerator: 'CmdOrCtrl+V', role: 'paste' }, { accelerator: 'CmdOrCtrl+A', role: 'selectall' } ] }, { label: 'Window', submenu: [ { accelerator: 'CmdOrCtrl+M', role: 'minimize' }, { accelerator: 'Command+W', role: 'close' } ] } ]; const menu = electron.Menu.buildFromTemplate(template); electron.Menu.setApplicationMenu(menu); } else { mainWindow.setMenuBarVisibility(false); } } function onContextMenu(e, props) { if (props.inputFieldType !== 'plainText' || !props.isEditable) { return; } const Menu = electron.Menu; const inputMenu = Menu.buildFromTemplate([ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { type: 'separator' }, { role: 'selectall' } ]); inputMenu.popup(mainWindow); } function notifyOpenFile() { if (ready && openFile && mainWindow) { const openKeyfile = process.argv .filter(arg => arg.startsWith('--keyfile=')) .map(arg => arg.replace('--keyfile=', ''))[0]; const fileInfo = JSON.stringify({ data: openFile, key: openKeyfile }); mainWindow.webContents.executeJavaScript( 'if (window.launcherOpen) { window.launcherOpen(' + fileInfo + '); } ' + ' else { window.launcherOpenedFile=' + fileInfo + '; }' ); openFile = null; } } function setGlobalShortcuts() { const shortcutModifiers = process.platform === 'darwin' ? 'Ctrl+Alt+' : 'Shift+Alt+'; const shortcuts = { C: 'copy-password', B: 'copy-user', U: 'copy-url', T: 'auto-type' }; Object.keys(shortcuts).forEach(key => { const shortcut = shortcutModifiers + key; const eventName = shortcuts[key]; try { electron.globalShortcut.register(shortcut, () => { emitBackboneEvent(eventName); }); } catch (e) {} }); } function subscribePowerEvents() { electron.powerMonitor.on('suspend', () => { emitBackboneEvent('power-monitor-suspend'); }); electron.powerMonitor.on('resume', () => { emitBackboneEvent('power-monitor-resume'); }); if (process.platform === 'darwin') { const id = electron.systemPreferences.subscribeNotification( 'com.apple.screenIsLocked', () => { emitBackboneEvent('os-lock'); } ); systemNotificationIds.push(id); } } function setEnv() { if ( process.platform === 'linux' && ['Pantheon', 'Unity:Unity7'].indexOf(process.env.XDG_CURRENT_DESKTOP) !== -1 ) { // https://github.com/electron/electron/issues/9046 process.env.XDG_CURRENT_DESKTOP = 'Unity'; } } function restorePreferences() { const profileConfigPath = path.join(userDataDir, 'profile.json'); const newProfile = { dir: tempUserDataPathRand }; let oldProfile; try { oldProfile = JSON.parse(fs.readFileSync(profileConfigPath, 'utf8')); } catch (e) {} fs.writeFileSync(profileConfigPath, JSON.stringify(newProfile)); if (oldProfile && oldProfile.dir && /^[\d.]+$/.test(oldProfile.dir)) { const oldProfilePath = path.join(tempUserDataPath, oldProfile.dir); const newProfilePath = path.join(tempUserDataPath, newProfile.dir); if (fs.existsSync(path.join(oldProfilePath, 'Cookies'))) { fs.mkdirSync(newProfilePath); const cookiesFileSrc = path.join(oldProfilePath, 'Cookies'); const cookiesFileDest = path.join(newProfilePath, 'Cookies'); try { fs.renameSync(cookiesFileSrc, cookiesFileDest); } catch (e) { try { fs.copyFileSync(cookiesFileSrc, cookiesFileDest); } catch (e) {} } } } } function deleteOldTempFiles() { if (app.oldTempFilesDeleted) { return; } setTimeout(() => { for (const dir of fs.readdirSync(tempUserDataPath)) { if (dir !== tempUserDataPathRand) { try { deleteRecursive(path.join(tempUserDataPath, dir)); } catch (e) {} } } app.oldTempFilesDeleted = true; // this is added to prevent file deletion on restart }, 1000); } function deleteRecursive(dir) { for (const file of fs.readdirSync(dir)) { const filePath = path.join(dir, file); if (fs.lstatSync(filePath).isDirectory()) { deleteRecursive(filePath); } else { fs.unlinkSync(filePath); } } fs.rmdirSync(dir); } // When sending a PUT XMLHttpRequest Chromium includes the header "Origin: file://". // This confuses some WebDAV clients, notably OwnCloud. // The header is invalid, so removing it everywhere it occurs should do no harm. function hookRequestHeaders() { electron.session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { if (!details.url.startsWith('ws:')) { delete details.requestHeaders.Origin; } callback({ cancel: false, requestHeaders: details.requestHeaders }); }); } // If a display is disconnected while KeeWeb is minimized, Electron does not // ensure that the restored window appears on a display that is still connected. // This checks to be sure the title bar is somewhere the user can grab it, // without making it impossible to minimize and restore a window keeping it // partially off-screen or straddling two displays if the user desires that. function coerceMainWindowPositionToConnectedDisplay() { const eScreen = electron.screen; const displays = eScreen.getAllDisplays(); if (!displays || !displays.length) return; const windowBounds = mainWindow.getBounds(); const contentBounds = mainWindow.getContentBounds(); const tbLeft = windowBounds.x; const tbRight = windowBounds.x + windowBounds.width; const tbTop = windowBounds.y; const tbBottom = contentBounds.y; // 160px width and 2/3s the title bar height should be enough that the user can grab it for (let i = 0; i < displays.length; ++i) { const workArea = displays[i].workArea; const overlapWidth = Math.min(tbRight, workArea.x + workArea.width) - Math.max(tbLeft, workArea.x); const overlapHeight = Math.min(tbBottom, workArea.y + workArea.height) - Math.max(tbTop, workArea.y); if (overlapWidth >= 160 && 3 * overlapHeight >= 2 * (tbBottom - tbTop)) return; } // If we get here, no display contains a big enough strip of the title bar // that we can be confident the user can drag it into visibility. Rather than // attempt to guess what the user wants, just center it on the primary display. // Try to keep the previous height and width, but clamp each to 90% of the workarea. const workArea = eScreen.getPrimaryDisplay().workArea; const newWidth = Math.min(windowBounds.width, Math.floor(0.9 * workArea.width)); const newHeight = Math.min(windowBounds.height, Math.floor(0.9 * workArea.height)); mainWindow.setBounds({ 'x': workArea.x + Math.floor((workArea.width - newWidth) / 2), 'y': workArea.y + Math.floor((workArea.height - newHeight) / 2), 'width': newWidth, 'height': newHeight }); updateMainWindowPosition(); }