From 3947bdc4aa91f864d2bd02df3f043cf273ca1329 Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Wed, 16 Feb 2022 19:28:23 +0100 Subject: [PATCH] chore: move electron out of the renderer (#4406) * fix lint * fix types * remove flush db.change event send * guard against tests running electron functions * copy preload to build output * fix webview context menu * fix context menu and plugin install * move installPlugin to main context --- packages/insomnia-app/app/common/database.ts | 10 +-- .../app/common/electron-helpers.ts | 52 +--------------- .../app/common/select-file-or-folder.ts | 5 +- packages/insomnia-app/app/global.d.ts | 17 +++++ packages/insomnia-app/app/main.development.ts | 62 ++++++++++++++++++- .../insomnia-app/app/main/window-utils.ts | 6 +- .../app/network/__tests__/multipart.test.ts | 3 +- .../app/network/__tests__/network.test.ts | 2 + .../insomnia-app/app/network/multipart.ts | 3 +- .../grant-authorization-code.test.ts | 7 +-- .../o-auth-2/__tests__/grant-implicit.test.ts | 4 +- .../app/network/o-auth-2/__tests__/helpers.ts | 2 +- .../network/o-auth-2/__tests__/misc.test.ts | 6 +- .../o-auth-2/grant-authorization-code.ts | 11 ++-- .../app/network/o-auth-2/grant-implicit.ts | 12 ++-- .../insomnia-app/app/network/o-auth-2/misc.ts | 26 ++++---- .../insomnia-app/app/plugins/context/app.tsx | 4 +- packages/insomnia-app/app/plugins/install.ts | 5 +- packages/insomnia-app/app/preload.js | 32 ++++++++++ .../components/editors/body/file-editor.tsx | 3 +- .../editors/body/graph-ql-editor.tsx | 4 +- .../app/ui/components/panes/response-pane.tsx | 8 +-- .../app/ui/components/request-url-bar.tsx | 4 +- .../app/ui/components/settings/plugins.tsx | 8 +-- .../viewers/response-multipart-viewer.tsx | 6 +- .../components/viewers/response-web-view.tsx | 15 +---- .../insomnia-app/app/ui/containers/app.tsx | 4 +- .../app/ui/hooks/use-settings-side-effects.ts | 5 +- .../app/ui/redux/modules/global.tsx | 8 +-- .../app/ui/redux/modules/import.ts | 2 +- packages/insomnia-app/package-lock.json | 16 ++--- packages/insomnia-app/package.json | 2 +- .../webpack/webpack.config.electron.ts | 12 +++- 33 files changed, 209 insertions(+), 157 deletions(-) create mode 100644 packages/insomnia-app/app/preload.js diff --git a/packages/insomnia-app/app/common/database.ts b/packages/insomnia-app/app/common/database.ts index 1bd182b65..0c7b7359a 100644 --- a/packages/insomnia-app/app/common/database.ts +++ b/packages/insomnia-app/app/common/database.ts @@ -247,12 +247,14 @@ export const database = { for (const fn of changeListeners) { await fn(changes); } - // Notify remote listeners - const windows = electron.BrowserWindow.getAllWindows(); + const isMainContext = process.type === 'browser'; + if (isMainContext){ + const windows = electron.BrowserWindow.getAllWindows(); - for (const window of windows) { - window.webContents.send('db.changes', changes); + for (const window of windows) { + window.webContents.send('db.changes', changes); + } } }, diff --git a/packages/insomnia-app/app/common/electron-helpers.ts b/packages/insomnia-app/app/common/electron-helpers.ts index 085ad78fb..bb0d9ae84 100644 --- a/packages/insomnia-app/app/common/electron-helpers.ts +++ b/packages/insomnia-app/app/common/electron-helpers.ts @@ -19,66 +19,18 @@ export function clickLink(href: string) { export const getPortableExecutableDir = () => process.env.PORTABLE_EXECUTABLE_DIR; export function getDataDirectory() { - const { app } = electron.remote || electron; + const { app } = process.type === 'renderer' ? window : electron; return process.env.INSOMNIA_DATA_PATH || app.getPath('userData'); } -export function getViewportSize(): string | null { - const { BrowserWindow } = electron.remote || electron; - const browserWindow = BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0]; - - if (browserWindow) { - const { width, height } = browserWindow.getContentBounds(); - return `${width}x${height}`; - } else { - // No windows open - return null; - } -} - -export function getScreenResolution() { - const { screen } = electron.remote || electron; - const { width, height } = screen.getPrimaryDisplay().workAreaSize; - return `${width}x${height}`; -} - -export function getUserLanguage() { - const { app } = electron.remote || electron; - return app.getLocale(); -} - export function getTempDir() { // NOTE: Using a fairly unique name here because "insomnia" is a common word - const { app } = electron.remote || electron; + const { app } = process.type === 'renderer' ? window : electron; const dir = join(app.getPath('temp'), `insomnia_${appConfig.version}`); mkdirp.sync(dir); return dir; } -export function restartApp() { - const { app } = electron.remote || electron; - app.relaunch(); - app.exit(); -} - -export const exitAppFailure = () => { - const { app } = electron.remote || electron; - app.exit(1); -}; - -export const setMenuBarVisibility = (visible: boolean) => { - const { BrowserWindow } = electron.remote || electron; - BrowserWindow.getAllWindows() - .forEach(window => { - // the `setMenuBarVisibility` signature uses `visible` semantics - window.setMenuBarVisibility(visible); - - // the `setAutoHideMenu` signature uses `hide` semantics - const hide = !visible; - window.setAutoHideMenuBar(hide); - }); -}; - /** * There's no option that prevents Electron from fetching spellcheck dictionaries from Chromium's CDN and passing a non-resolving URL is the only known way to prevent it from fetching. * see: https://github.com/electron/electron/issues/22995 diff --git a/packages/insomnia-app/app/common/select-file-or-folder.ts b/packages/insomnia-app/app/common/select-file-or-folder.ts index 3cc310ba7..62386bde8 100644 --- a/packages/insomnia-app/app/common/select-file-or-folder.ts +++ b/packages/insomnia-app/app/common/select-file-or-folder.ts @@ -1,4 +1,4 @@ -import { OpenDialogOptions, remote } from 'electron'; +import { OpenDialogOptions } from 'electron'; import { unreachableCase } from 'ts-assert-unreachable'; interface Options { @@ -49,7 +49,8 @@ export const selectFileOrFolder = async ({ itemTypes, extensions }: Options) => }], }; - const { canceled, filePaths } = await remote.dialog.showOpenDialog(options); + const { canceled, filePaths } = await window.dialog.showOpenDialog(options); + const fileSelection: FileSelection = { filePath: filePaths[0], canceled, diff --git a/packages/insomnia-app/app/global.d.ts b/packages/insomnia-app/app/global.d.ts index 3aaebbebd..4ee9ce60c 100644 --- a/packages/insomnia-app/app/global.d.ts +++ b/packages/insomnia-app/app/global.d.ts @@ -22,6 +22,23 @@ declare namespace NodeJS { interface Window { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: Function; + main: { + restart: () => void; + authorizeUserInWindow: (options: { url: string; urlSuccessRegex?: RegExp; urlFailureRegex?: RegExp; sessionId: string }) => Promise; + setMenuBarVisibility: (visible: boolean) => void; + installPlugin: (url: string) => void; + }; + dialog: { + showOpenDialog: (options: Electron.OpenDialogOptions) => Promise; + showSaveDialog: (options: Electron.SaveDialogOptions) => Promise; + }; + app: { + getPath: (name: string) => string; + getAppPath: () => string; + }; + shell: { + showItemInFolder: (fullPath: string) => void; + }; } // needed for @hot-loader/react-dom in order for TypeScript to build diff --git a/packages/insomnia-app/app/main.development.ts b/packages/insomnia-app/app/main.development.ts index a37ab8efd..efc17cce2 100644 --- a/packages/insomnia-app/app/main.development.ts +++ b/packages/insomnia-app/app/main.development.ts @@ -7,7 +7,7 @@ import appConfig from '../config/config.json'; import { SegmentEvent, trackSegmentEvent } from './common/analytics'; import { changelogUrl, getAppVersion, isDevelopment, isMac } from './common/constants'; import { database } from './common/database'; -import { disableSpellcheckerDownload, exitAppFailure } from './common/electron-helpers'; +import { disableSpellcheckerDownload } from './common/electron-helpers'; import log, { initializeLogging } from './common/log'; import { validateInsomniaConfig } from './common/validate-insomnia-config'; import * as errorHandling from './main/error-handling'; @@ -17,6 +17,8 @@ import * as updates from './main/updates'; import * as windowUtils from './main/window-utils'; import * as models from './models/index'; import type { Stats } from './models/stats'; +import { authorizeUserInWindow } from './network/o-auth-2/misc'; +import installPlugin from './plugins/install'; import type { ToastNotification } from './ui/components/toast'; // Handle potential auto-update @@ -41,7 +43,14 @@ if (!isDevelopment()) { // So if (window) checks don't throw global.window = global.window || undefined; -contextMenu(); +// setup right click menu +app.on('web-contents-created', (_, contents) => { + if (contents.getType() === 'webview') { + contextMenu({ window: contents }); + } else { + contextMenu(); + } +}); // When the app is first launched app.on('ready', async () => { @@ -50,7 +59,7 @@ app.on('ready', async () => { if (error) { electron.dialog.showErrorBox(error.title, error.message); console.log('[config] Insomnia config is invalid, preventing app initialization'); - exitAppFailure(); + app.exit(1); return; } @@ -203,6 +212,53 @@ async function _trackStats() { trackSegmentEvent(SegmentEvent.appStarted, {}, { queueable: true }); + ipcMain.handle('showOpenDialog', async (_, options: Electron.OpenDialogOptions) => { + const { filePaths, canceled } = await electron.dialog.showOpenDialog(options); + return { filePaths, canceled }; + }); + + ipcMain.handle('showSaveDialog', async (_, options: Electron.SaveDialogOptions) => { + const { filePath, canceled } = await electron.dialog.showSaveDialog(options); + return { filePath, canceled }; + }); + + ipcMain.handle('installPlugin', async (_, options) => { + return installPlugin(options); + }); + + ipcMain.on('showItemInFolder', (_, name) => { + electron.shell.showItemInFolder(name); + }); + + ipcMain.on('restart', () => { + app.relaunch(); + app.exit(); + }); + + ipcMain.handle('setMenuBarVisibility', (_, visible) => { + electron.BrowserWindow.getAllWindows() + .forEach(window => { + // the `setMenuBarVisibility` signature uses `visible` semantics + window.setMenuBarVisibility(visible); + // the `setAutoHideMenu` signature uses `hide` semantics + const hide = !visible; + window.setAutoHideMenuBar(hide); + }); + }); + + ipcMain.on('getPath', (event, name) => { + event.returnValue = electron.app.getPath(name); + }); + + ipcMain.on('getAppPath', event => { + event.returnValue = electron.app.getAppPath(); + }); + + ipcMain.handle('authorizeUserInWindow', (_, options) => { + const { url, urlSuccessRegex, urlFailureRegex, sessionId } = options; + return authorizeUserInWindow({ url, urlSuccessRegex, urlFailureRegex, sessionId }); + }); + ipcMain.once('window-ready', () => { const { currentVersion, launches, lastVersion } = stats; diff --git a/packages/insomnia-app/app/main/window-utils.ts b/packages/insomnia-app/app/main/window-utils.ts index db916e266..726d65382 100644 --- a/packages/insomnia-app/app/main/window-utils.ts +++ b/packages/insomnia-app/app/main/window-utils.ts @@ -17,7 +17,7 @@ import { MNEMONIC_SYM, } from '../common/constants'; import { docsBase } from '../common/documentation'; -import { clickLink, getDataDirectory, restartApp } from '../common/electron-helpers'; +import { clickLink, getDataDirectory } from '../common/electron-helpers'; import * as log from '../common/log'; import LocalStorage from './local-storage'; @@ -85,10 +85,10 @@ export function createWindow() { acceptFirstMouse: true, icon: path.resolve(__dirname, appLogo), webPreferences: { + preload: path.join(__dirname, 'preload.js'), zoomFactor: zoomFactor, nodeIntegration: true, webviewTag: true, - enableRemoteModule: true, // TODO: enable context isolation contextIsolation: false, disableBlinkFeatures: 'Auxclick', @@ -482,7 +482,7 @@ export function createWindow() { }, { label: `R${MNEMONIC_SYM}estart`, - click: restartApp, + click: window?.main.restart, }, ], }; diff --git a/packages/insomnia-app/app/network/__tests__/multipart.test.ts b/packages/insomnia-app/app/network/__tests__/multipart.test.ts index 34ab07a15..ec3425e73 100644 --- a/packages/insomnia-app/app/network/__tests__/multipart.test.ts +++ b/packages/insomnia-app/app/network/__tests__/multipart.test.ts @@ -1,9 +1,10 @@ +import electron from 'electron'; import fs from 'fs'; import path from 'path'; import { globalBeforeEach } from '../../__jest__/before-each'; import { buildMultipart, DEFAULT_BOUNDARY } from '../multipart'; - +window.app = electron.app; describe('buildMultipart()', () => { beforeEach(globalBeforeEach); diff --git a/packages/insomnia-app/app/network/__tests__/network.test.ts b/packages/insomnia-app/app/network/__tests__/network.test.ts index 21de75586..021c56dd8 100644 --- a/packages/insomnia-app/app/network/__tests__/network.test.ts +++ b/packages/insomnia-app/app/network/__tests__/network.test.ts @@ -1,4 +1,5 @@ import { CurlHttpVersion } from '@getinsomnia/node-libcurl'; +import electron from 'electron'; import fs from 'fs'; import { HttpVersions } from 'insomnia-common'; import { join as pathJoin, resolve as pathResolve } from 'path'; @@ -18,6 +19,7 @@ import { getRenderedRequestAndContext } from '../../common/render'; import * as models from '../../models'; import { DEFAULT_BOUNDARY } from '../multipart'; import * as networkUtils from '../network'; +window.app = electron.app; const getRenderedRequest = async (args: Parameters[0]) => (await getRenderedRequestAndContext(args)).request; diff --git a/packages/insomnia-app/app/network/multipart.ts b/packages/insomnia-app/app/network/multipart.ts index dbfddba72..723cbad46 100644 --- a/packages/insomnia-app/app/network/multipart.ts +++ b/packages/insomnia-app/app/network/multipart.ts @@ -1,4 +1,3 @@ -import * as electron from 'electron'; import fs from 'fs'; import mimes from 'mime-types'; import path from 'path'; @@ -15,7 +14,7 @@ interface Multipart { export async function buildMultipart(params: RequestBodyParameter[]) { return new Promise(async (resolve, reject) => { - const filePath = path.join(electron.remote.app.getPath('temp'), Math.random() + '.body'); + const filePath = path.join(window.app.getPath('temp'), Math.random() + '.body'); const writeStream = fs.createWriteStream(filePath); const lineBreak = '\r\n'; let totalSize = 0; diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts index 5e1398c6b..4c4624411 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts @@ -5,7 +5,6 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { getTempDir } from '../../../common/electron-helpers'; import * as network from '../../network'; import getToken from '../grant-authorization-code'; -import { createBWRedirectMock } from './helpers'; // Mock some test things const AUTHORIZE_URL = 'https://foo.com/authorizeAuthCode'; @@ -22,7 +21,7 @@ describe('authorization_code', () => { beforeEach(globalBeforeEach); it('gets token with JSON and basic auth', async () => { - createBWRedirectMock({ redirectTo: `${REDIRECT_URI}?code=code_123&state=${STATE}` }); + window.main = { authorizeUserInWindow: () => Promise.resolve(`${REDIRECT_URI}?code=code_123&state=${STATE}`) }; const bodyPath = path.join(getTempDir(), 'foo.response'); fs.writeFileSync( bodyPath, @@ -130,7 +129,7 @@ describe('authorization_code', () => { }); it('gets token with urlencoded and body auth', async () => { - createBWRedirectMock({ redirectTo: `${REDIRECT_URI}?code=code_123&state=${STATE}` }); + window.main = { authorizeUserInWindow: () => Promise.resolve(`${REDIRECT_URI}?code=code_123&state=${STATE}`) }; const bodyPath = path.join(getTempDir(), 'foo.response'); fs.writeFileSync( bodyPath, @@ -242,7 +241,7 @@ describe('authorization_code', () => { }); it('uses PKCE', async () => { - createBWRedirectMock({ redirectTo: `${REDIRECT_URI}?code=code_123&state=${STATE}` }); + window.main = { authorizeUserInWindow: () => Promise.resolve(`${REDIRECT_URI}?code=code_123&state=${STATE}`) }; const bodyPath = path.join(getTempDir(), 'foo.response'); fs.writeFileSync( bodyPath, diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts index ee261a37b..8bf989fce 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts @@ -1,6 +1,5 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import getToken from '../grant-implicit'; -import { createBWRedirectMock } from './helpers'; // Mock some test things const AUTHORIZE_URL = 'https://foo.com/authorizeAuthCode'; const CLIENT_ID = 'client_123'; @@ -13,7 +12,8 @@ describe('implicit', () => { beforeEach(globalBeforeEach); it('works in default case', async () => { - createBWRedirectMock({ redirectTo: `${REDIRECT_URI}#access_token=token_123&state=${STATE}&foo=bar` }); + window.main = { authorizeUserInWindow: () => Promise.resolve(`${REDIRECT_URI}#access_token=token_123&state=${STATE}&foo=bar`) }; + const result = await getToken(AUTHORIZE_URL, CLIENT_ID, REDIRECT_URI, SCOPE, STATE, AUDIENCE); expect(result).toEqual({ access_token: 'token_123', diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts b/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts index fcefe4f8a..def391cf5 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts @@ -10,7 +10,7 @@ export function createBWRedirectMock({ redirectTo, setCertificateVerifyProc = () => {}, }: Options) { - electron.remote.BrowserWindow = jest.fn(function() { + electron.BrowserWindow = jest.fn(function() { this._emitter = new EventEmitter(); this.loadURL = () => this.webContents.emit('did-navigate'); diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts b/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts index 8a9c9fa37..04e0cf53a 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts @@ -64,6 +64,8 @@ describe('authorizeUserInWindow()', () => { const getCertificateVerifyCallbackMock = () => { const mockCallback = mocked<(verificationResult: number) => void>(jest.fn()); + window.main = { authorizeUserInWindow: () => Promise.resolve(MOCK_AUTHORIZATION_URL) }; + createBWRedirectMock({ redirectTo: MOCK_AUTHORIZATION_URL, setCertificateVerifyProc: proc => { @@ -86,7 +88,7 @@ describe('authorizeUserInWindow()', () => { // Act // We don't really care about the result here, since we're only testing an event handler. - await authorizeUserInWindow(MOCK_AUTHORIZATION_URL, /.*/); + await authorizeUserInWindow({ url: MOCK_AUTHORIZATION_URL, urlSuccessRegex: /.*/ }); // Assert expect(mockCallback).toHaveBeenCalledWith(ChromiumVerificationResult.USE_CHROMIUM_RESULT); @@ -103,7 +105,7 @@ describe('authorizeUserInWindow()', () => { // Act // We don't really care about the result here, since we're only testing an event handler. - await authorizeUserInWindow(MOCK_AUTHORIZATION_URL, /.*/); + await authorizeUserInWindow({ url: MOCK_AUTHORIZATION_URL, urlSuccessRegex: /.*/ }); // Assert expect(mockCallback).toHaveBeenCalledWith(ChromiumVerificationResult.BLIND_TRUST); diff --git a/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts b/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts index f112da4e8..f5623c247 100644 --- a/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts +++ b/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts @@ -7,8 +7,7 @@ import * as models from '../../models/index'; import { getBasicAuthHeader } from '../basic-auth/get-header'; import { sendWithSettings } from '../network'; import * as c from './constants'; -import { authorizeUserInWindow, responseToObject } from './misc'; - +import { getOAuthSession, responseToObject } from './misc'; export default async function( requestId: string, authorizeUrl: string, @@ -147,9 +146,11 @@ async function _authorize( // Add query params to URL const qs = buildQueryStringFromParams(params); const finalUrl = joinUrlAndQueryString(url, qs); - const successRegex = new RegExp(`${escapeRegex(redirectUri)}.*(code=)`, 'i'); - const failureRegex = new RegExp(`${escapeRegex(redirectUri)}.*(error=)`, 'i'); - const redirectedTo = await authorizeUserInWindow(finalUrl, successRegex, failureRegex); + const urlSuccessRegex = new RegExp(`${escapeRegex(redirectUri)}.*(code=)`, 'i'); + const urlFailureRegex = new RegExp(`${escapeRegex(redirectUri)}.*(error=)`, 'i'); + const sessionId = getOAuthSession(); + + const redirectedTo = await window.main.authorizeUserInWindow({ url: finalUrl, urlSuccessRegex, urlFailureRegex, sessionId }); console.log('[oauth2] Detected redirect ' + redirectedTo); const { query } = urlParse(redirectedTo); return responseToObject(query, [ diff --git a/packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts b/packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts index e7155f49a..ad19dcc4b 100644 --- a/packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts +++ b/packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts @@ -1,7 +1,7 @@ import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url'; import * as c from './constants'; -import { authorizeUserInWindow, responseToObject } from './misc'; +import { getOAuthSession, responseToObject } from './misc'; export default async function( _requestId: string, @@ -60,11 +60,11 @@ export default async function( // Add query params to URL const qs = buildQueryStringFromParams(params); const finalUrl = joinUrlAndQueryString(authorizationUrl, qs); - const redirectedTo = await authorizeUserInWindow( - finalUrl, - /(access_token=|id_token=)/, - /(error=)/, - ); + const urlSuccessRegex = /(access_token=|id_token=)/; + const urlFailureRegex = /(error=)/; + const sessionId = getOAuthSession(); + const redirectedTo = await window.main.authorizeUserInWindow({ url: finalUrl, urlSuccessRegex, urlFailureRegex, sessionId }); + const fragment = redirectedTo.split('#')[1]; if (fragment) { diff --git a/packages/insomnia-app/app/network/o-auth-2/misc.ts b/packages/insomnia-app/app/network/o-auth-2/misc.ts index 1e92b77d3..7a6327598 100644 --- a/packages/insomnia-app/app/network/o-auth-2/misc.ts +++ b/packages/insomnia-app/app/network/o-auth-2/misc.ts @@ -1,4 +1,4 @@ -import electron from 'electron'; +import { BrowserWindow } from 'electron'; import querystring from 'querystring'; import * as uuid from 'uuid'; @@ -9,20 +9,17 @@ export enum ChromiumVerificationResult { USE_CHROMIUM_RESULT = -3 } -const LOCALSTORAGE_KEY_SESSION_ID = 'insomnia::current-oauth-session-id'; -let authWindowSessionId; - -if (window.localStorage.getItem(LOCALSTORAGE_KEY_SESSION_ID)) { - authWindowSessionId = window.localStorage.getItem(LOCALSTORAGE_KEY_SESSION_ID); -} else { - initNewOAuthSession(); +export const LOCALSTORAGE_KEY_SESSION_ID = 'insomnia::current-oauth-session-id'; +export function getOAuthSession(): string { + const token = window.localStorage.getItem(LOCALSTORAGE_KEY_SESSION_ID); + return token || initNewOAuthSession(); } - export function initNewOAuthSession() { // the value of this variable needs to start with 'persist:' // otherwise sessions won't be persisted over application-restarts - authWindowSessionId = `persist:oauth2_${uuid.v4()}`; + const authWindowSessionId = `persist:oauth2_${uuid.v4()}`; window.localStorage.setItem(LOCALSTORAGE_KEY_SESSION_ID, authWindowSessionId); + return authWindowSessionId; } export function responseToObject(body, keys, defaults = {}) { @@ -60,11 +57,12 @@ export function responseToObject(body, keys, defaults = {}) { return results; } -export function authorizeUserInWindow( +export function authorizeUserInWindow({ url, urlSuccessRegex = /(code=).*/, urlFailureRegex = /(error=).*/, -) { + sessionId, +}) { return new Promise(async (resolve, reject) => { let finalUrl: string | null = null; @@ -74,10 +72,10 @@ export function authorizeUserInWindow( } = await models.settings.getOrCreate(); // Create a child window - const child = new electron.remote.BrowserWindow({ + const child = new BrowserWindow({ webPreferences: { nodeIntegration: false, - partition: authWindowSessionId, + partition: sessionId, }, show: false, }); diff --git a/packages/insomnia-app/app/plugins/context/app.tsx b/packages/insomnia-app/app/plugins/context/app.tsx index e0504c52d..4993fe2c2 100644 --- a/packages/insomnia-app/app/plugins/context/app.tsx +++ b/packages/insomnia-app/app/plugins/context/app.tsx @@ -146,7 +146,7 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { getPath(name: string) { switch (name.toLowerCase()) { case 'desktop': - return electron.remote.app.getPath('desktop'); + return window.app.getPath('desktop'); default: throw new Error(`Unknown path name ${name}`); @@ -172,7 +172,7 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { buttonLabel: 'Save', defaultPath: options.defaultPath, }; - const { filePath } = await electron.remote.dialog.showSaveDialog( + const { filePath } = await window.dialog.showSaveDialog( saveOptions ); return filePath || null; diff --git a/packages/insomnia-app/app/plugins/install.ts b/packages/insomnia-app/app/plugins/install.ts index 54698f83c..0935166dc 100644 --- a/packages/insomnia-app/app/plugins/install.ts +++ b/packages/insomnia-app/app/plugins/install.ts @@ -56,10 +56,11 @@ export default async function(lookupName: string) { mkdirp.sync(pluginDir); // Download the module - const request = electron.remote.net.request(info.dist.tarball); + const request = electron.net.request(info.dist.tarball); request.on('error', err => { reject(new Error(`Failed to make plugin request ${info?.dist.tarball}: ${err.message}`)); }); + const { tmpDir } = await _installPluginToTmpDir(lookupName); console.log(`[plugins] Moving plugin from ${tmpDir} to ${pluginDir}`); @@ -247,7 +248,7 @@ export function isDeprecatedDependencies(str: string) { } function _getYarnPath() { - const { app } = electron.remote || electron; + const { app } = process.type === 'renderer' ? window : electron; // TODO: This is brittle. Make finding this more robust. if (isDevelopment()) { diff --git a/packages/insomnia-app/app/preload.js b/packages/insomnia-app/app/preload.js new file mode 100644 index 000000000..a78004551 --- /dev/null +++ b/packages/insomnia-app/app/preload.js @@ -0,0 +1,32 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +const main = { + restart: () => ipcRenderer.send('restart'), + authorizeUserInWindow: options => ipcRenderer.invoke('authorizeUserInWindow', options), + setMenuBarVisibility: options => ipcRenderer.send('setMenuBarVisibility', options), + installPlugin: options => ipcRenderer.invoke('installPlugin', options), +}; +const dialog = { + showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options), + showSaveDialog: options => ipcRenderer.invoke('showSaveDialog', options), +}; +const app = { + getPath: options => ipcRenderer.sendSync('getPath', options), + getAppPath: options => ipcRenderer.sendSync('getAppPath', options), +}; +const shell = { + showItemInFolder: options => ipcRenderer.send('showItemInFolder', options), +}; + +// if (process.contextIsolated) { TODO: use if rather than try after upgrading to electron 13 +try { + contextBridge.exposeInMainWorld('main', main); + contextBridge.exposeInMainWorld('dialog', dialog); + contextBridge.exposeInMainWorld('app', app); + contextBridge.exposeInMainWorld('shell', shell); +} catch { + window.main = main; + window.dialog = dialog; + window.app = app; + window.shell = shell; +} diff --git a/packages/insomnia-app/app/ui/components/editors/body/file-editor.tsx b/packages/insomnia-app/app/ui/components/editors/body/file-editor.tsx index c04c7f502..ad338b703 100644 --- a/packages/insomnia-app/app/ui/components/editors/body/file-editor.tsx +++ b/packages/insomnia-app/app/ui/components/editors/body/file-editor.tsx @@ -1,5 +1,4 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import electron from 'electron'; import fs from 'fs'; import React, { PureComponent } from 'react'; @@ -26,7 +25,7 @@ export class FileEditor extends PureComponent { render() { const { path } = this.props; // Replace home path with ~/ to make the path shorter - const homeDirectory = electron.remote.app.getPath('home'); + const homeDirectory = window.app.getPath('home'); const pathDescription = path.replace(homeDirectory, '~'); let sizeDescription = ''; diff --git a/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx index f9efb35ab..db48f69e5 100644 --- a/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx @@ -3,7 +3,7 @@ import classnames from 'classnames'; import { EditorFromTextArea, LintOptions, ShowHintOptions, TextMarker } from 'codemirror'; import { GraphQLInfoOptions } from 'codemirror-graphql/info'; import { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; -import electron, { OpenDialogOptions } from 'electron'; +import { OpenDialogOptions } from 'electron'; import { readFileSync } from 'fs'; import { DefinitionNode, DocumentNode, GraphQLNonNull, GraphQLSchema, NonNullTypeNode, OperationDefinitionNode } from 'graphql'; import { parse, typeFromAST } from 'graphql'; @@ -362,7 +362,7 @@ export class GraphQLEditor extends PureComponent { ], }; - const { canceled, filePaths } = await electron.remote.dialog.showOpenDialog(options); + const { canceled, filePaths } = await window.dialog.showOpenDialog(options); if (canceled) { return; diff --git a/packages/insomnia-app/app/ui/components/panes/response-pane.tsx b/packages/insomnia-app/app/ui/components/panes/response-pane.tsx index 2260119b0..fee78edad 100644 --- a/packages/insomnia-app/app/ui/components/panes/response-pane.tsx +++ b/packages/insomnia-app/app/ui/components/panes/response-pane.tsx @@ -1,6 +1,6 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; import classnames from 'classnames'; -import { clipboard, remote } from 'electron'; +import { clipboard } from 'electron'; import fs from 'fs'; import { HotKeyRegistry } from 'insomnia-common'; import { json as jsonPrettify } from 'insomnia-prettify'; @@ -85,7 +85,7 @@ export class ResponsePane extends PureComponent { const { contentType } = response; const extension = mime.extension(contentType) || 'unknown'; - const { canceled, filePath: outputPath } = await remote.dialog.showSaveDialog({ + const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({ title: 'Save Response Body', buttonLabel: 'Save', defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.${extension}`, @@ -140,7 +140,7 @@ export class ResponsePane extends PureComponent { .map(v => v.value) .join(''); - const { canceled, filePath } = await remote.dialog.showSaveDialog({ + const { canceled, filePath } = await window.dialog.showSaveDialog({ title: 'Save Full Response', buttonLabel: 'Save', defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.txt`, @@ -192,7 +192,7 @@ export class ResponsePane extends PureComponent { const data = await exportHarCurrentRequest(request, response); const har = JSON.stringify(data, null, '\t'); - const { filePath } = await remote.dialog.showSaveDialog({ + const { filePath } = await window.dialog.showSaveDialog({ title: 'Export As HAR', buttonLabel: 'Save', defaultPath: `${request.name.replace(/ +/g, '_')}-${Date.now()}.har`, diff --git a/packages/insomnia-app/app/ui/components/request-url-bar.tsx b/packages/insomnia-app/app/ui/components/request-url-bar.tsx index 96ed08183..69847bc2c 100644 --- a/packages/insomnia-app/app/ui/components/request-url-bar.tsx +++ b/packages/insomnia-app/app/ui/components/request-url-bar.tsx @@ -1,5 +1,5 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import { OpenDialogOptions, remote } from 'electron'; +import { OpenDialogOptions } from 'electron'; import { HotKeyRegistry } from 'insomnia-common'; import React, { PureComponent, ReactNode } from 'react'; @@ -123,7 +123,7 @@ export class RequestUrlBar extends PureComponent { buttonLabel: 'Select', properties: ['openDirectory'], }; - const { canceled, filePaths } = await remote.dialog.showOpenDialog(options); + const { canceled, filePaths } = await window.dialog.showOpenDialog(options); if (canceled) { return; diff --git a/packages/insomnia-app/app/ui/components/settings/plugins.tsx b/packages/insomnia-app/app/ui/components/settings/plugins.tsx index 5faea696a..f51023830 100644 --- a/packages/insomnia-app/app/ui/components/settings/plugins.tsx +++ b/packages/insomnia-app/app/ui/components/settings/plugins.tsx @@ -1,5 +1,4 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import * as electron from 'electron'; import { PluginConfig } from 'insomnia-common'; import { Button, ToggleSwitch } from 'insomnia-components'; import * as path from 'path'; @@ -18,7 +17,6 @@ import type { Settings } from '../../../models/settings'; import { createPlugin } from '../../../plugins/create'; import type { Plugin } from '../../../plugins/index'; import { getPlugins } from '../../../plugins/index'; -import installPlugin from '../../../plugins/install'; import { reload } from '../../../templating/index'; import { CopyButton } from '../base/copy-button'; import { Link } from '../base/link'; @@ -76,7 +74,7 @@ export class Plugins extends PureComponent { }; try { - await installPlugin(this.state.npmPluginValue.trim()); + await window.main.installPlugin(this.state.npmPluginValue.trim()); await this._handleRefreshPlugins(); newState.npmPluginValue = ''; // Clear input if successful install } catch (err) { @@ -88,7 +86,7 @@ export class Plugins extends PureComponent { } static _handleOpenDirectory(directory: string) { - electron.remote.shell.showItemInFolder(directory); + window.shell.showItemInFolder(directory); } async _handleRefreshPlugins() { @@ -116,7 +114,7 @@ export class Plugins extends PureComponent { } static _handleClickShowPluginsFolder() { - electron.remote.shell.showItemInFolder(PLUGIN_PATH); + window.shell.showItemInFolder(PLUGIN_PATH); } _handleCreatePlugin() { diff --git a/packages/insomnia-app/app/ui/components/viewers/response-multipart-viewer.tsx b/packages/insomnia-app/app/ui/components/viewers/response-multipart-viewer.tsx index 240984368..b7f9440df 100644 --- a/packages/insomnia-app/app/ui/components/viewers/response-multipart-viewer.tsx +++ b/packages/insomnia-app/app/ui/components/viewers/response-multipart-viewer.tsx @@ -1,5 +1,5 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import electron, { SaveDialogOptions } from 'electron'; +import { SaveDialogOptions } from 'electron'; import fs from 'fs'; import mimes from 'mime-types'; import moment from 'moment'; @@ -132,7 +132,7 @@ export class ResponseMultipartViewer extends PureComponent { const contentType = getContentTypeFromHeaders(part.headers, 'text/plain'); const extension = mimes.extension(contentType) || '.txt'; const lastDir = window.localStorage.getItem('insomnia.lastExportPath'); - const dir = lastDir || electron.remote.app.getPath('desktop'); + const dir = lastDir || window.app.getPath('desktop'); const date = moment().format('YYYY-MM-DD'); const filename = part.filename || `${part.name}_${date}`; const options: SaveDialogOptions = { @@ -146,7 +146,7 @@ export class ResponseMultipartViewer extends PureComponent { }, ], }; - const { canceled, filePath } = await electron.remote.dialog.showSaveDialog(options); + const { canceled, filePath } = await window.dialog.showSaveDialog(options); if (canceled) { return; diff --git a/packages/insomnia-app/app/ui/components/viewers/response-web-view.tsx b/packages/insomnia-app/app/ui/components/viewers/response-web-view.tsx index 07e3f3532..1f5944cbc 100644 --- a/packages/insomnia-app/app/ui/components/viewers/response-web-view.tsx +++ b/packages/insomnia-app/app/ui/components/viewers/response-web-view.tsx @@ -1,6 +1,4 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import contextMenu from 'electron-context-menu'; -import { EventEmitter } from 'events'; import React, { createRef, PureComponent } from 'react'; import { AUTOBIND_CFG } from '../../../common/constants'; @@ -37,11 +35,6 @@ export class ResponseWebView extends PureComponent { this.webview.current.removeEventListener('dom-ready', this._handleDOMReady); - contextMenu({ - // @ts-expect-error -- TSCONVERSION type mismatch - window: this.webview.current, - }); - this._setBody(); } @@ -59,17 +52,11 @@ export class ResponseWebView extends PureComponent { // // https://github.com/electron/electron/issues/20700 // - // webview.loadURL(`data:${contentType},${encodeURIComponent(body)}`, { + // this.webview.current.loadURL(`data:${contentType},${encodeURIComponent(body)}`, { // baseURLForDataURL: url, // }); // @ts-expect-error -- TSCONVERSION type mismatch this.webview.current.loadURL(`data:${contentType},${encodeURIComponent(bodyWithBase)}`); - - // This is kind of hacky but electron-context-menu fails to save images if this isn't here. - // @ts-expect-error -- TSCONVERSION type mismatch - this.webview.current.webContents = this.webview.current; - // @ts-expect-error -- TSCONVERSION type mismatch - this.webview.current.webContents.session = new EventEmitter(); } render() { diff --git a/packages/insomnia-app/app/ui/containers/app.tsx b/packages/insomnia-app/app/ui/containers/app.tsx index 7cf714dda..743960aa0 100644 --- a/packages/insomnia-app/app/ui/containers/app.tsx +++ b/packages/insomnia-app/app/ui/containers/app.tsx @@ -1,5 +1,5 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import { clipboard, ipcRenderer, remote, SaveDialogOptions } from 'electron'; +import { clipboard, ipcRenderer, SaveDialogOptions } from 'electron'; import fs from 'fs'; import HTTPSnippet from 'httpsnippet'; import * as mime from 'mime-types'; @@ -681,7 +681,7 @@ class App extends PureComponent { options.defaultPath = defaultPath; } - const { filePath } = await remote.dialog.showSaveDialog(options); + const { filePath } = await window.dialog.showSaveDialog(options); // @ts-expect-error -- TSCONVERSION don't set item if filePath is undefined window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath); return filePath || null; diff --git a/packages/insomnia-app/app/ui/hooks/use-settings-side-effects.ts b/packages/insomnia-app/app/ui/hooks/use-settings-side-effects.ts index 6562dc0cc..8989e729c 100644 --- a/packages/insomnia-app/app/ui/hooks/use-settings-side-effects.ts +++ b/packages/insomnia-app/app/ui/hooks/use-settings-side-effects.ts @@ -2,7 +2,6 @@ import { useEffect, useLayoutEffect } from 'react'; import { useSelector } from 'react-redux'; import { usePrevious } from 'react-use'; -import { restartApp, setMenuBarVisibility } from '../../common/electron-helpers'; import { Settings } from '../../models/settings'; import { selectSettings } from '../redux/selectors'; @@ -22,7 +21,7 @@ const useRestartSetting = (setting: keyof Settings) => { return; } - restartApp(); + window.main.restart(); }, [nextValue, previousValue]); }; @@ -49,7 +48,7 @@ export const useSettingsSideEffects = () => { }, [settings.fontSize]); useEffect(() => { - setMenuBarVisibility(!settings.autoHideMenuBar); + window.main.setMenuBarVisibility(!settings.autoHideMenuBar); }, [settings.autoHideMenuBar]); useRestartSetting('nunjucksPowerUserMode'); diff --git a/packages/insomnia-app/app/ui/redux/modules/global.tsx b/packages/insomnia-app/app/ui/redux/modules/global.tsx index 306c5f570..bbfaee93a 100644 --- a/packages/insomnia-app/app/ui/redux/modules/global.tsx +++ b/packages/insomnia-app/app/ui/redux/modules/global.tsx @@ -1,4 +1,3 @@ -import electron from 'electron'; import fs, { NoParamCallback } from 'fs'; import moment from 'moment'; import path from 'path'; @@ -30,7 +29,6 @@ import { Request } from '../../../models/request'; import { isWorkspace } from '../../../models/workspace'; import { reloadPlugins } from '../../../plugins'; import { createPlugin } from '../../../plugins/create'; -import install from '../../../plugins/install'; import { setTheme } from '../../../plugins/misc'; import { AskModal } from '../../../ui/components/modals/ask-modal'; import { AlertModal } from '../../components/modals/alert-modal'; @@ -240,7 +238,7 @@ export const newCommand = (command: string, args: any) => async (dispatch: Dispa } try { - await install(args.name); + await window.main.installPlugin(args.name); showModal(SettingsModal, TAB_INDEX_PLUGINS); } catch (err) { showError({ @@ -416,13 +414,13 @@ const showSaveExportedFileDialog = async ({ const date = moment().format('YYYY-MM-DD'); const name = exportedFileNamePrefix.replace(/ /g, '-'); const lastDir = window.localStorage.getItem('insomnia.lastExportPath'); - const dir = lastDir || electron.remote.app.getPath('desktop'); + const dir = lastDir || window.app.getPath('desktop'); const options = { title: 'Export Insomnia Data', buttonLabel: 'Export', defaultPath: `${path.join(dir, `${name}_${date}`)}.${selectedFormat}`, }; - const { filePath } = await electron.remote.dialog.showSaveDialog(options); + const { filePath } = await window.dialog.showSaveDialog(options); return filePath || null; }; diff --git a/packages/insomnia-app/app/ui/redux/modules/import.ts b/packages/insomnia-app/app/ui/redux/modules/import.ts index 3ad5a0b82..1ac3e9711 100644 --- a/packages/insomnia-app/app/ui/redux/modules/import.ts +++ b/packages/insomnia-app/app/ui/redux/modules/import.ts @@ -89,7 +89,7 @@ export const importFile = ( }, ], }; - const { canceled, filePaths } = await electron.remote.dialog.showOpenDialog(openDialogOptions); + const { canceled, filePaths } = await window.dialog.showOpenDialog(openDialogOptions); if (canceled) { // It was cancelled, so let's bail out diff --git a/packages/insomnia-app/package-lock.json b/packages/insomnia-app/package-lock.json index 2e73ea600..89216dba9 100644 --- a/packages/insomnia-app/package-lock.json +++ b/packages/insomnia-app/package-lock.json @@ -11244,13 +11244,13 @@ } }, "electron-context-menu": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.5.2.tgz", - "integrity": "sha512-1cEQR6fA9ktFsRBc+eXPwvrOgAPytUD7rUV4iBAA5zTrLAPKokJ23xeMjcK2fjrDPrlFRBxcLz0KP+GUhMrSCQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-3.1.1.tgz", + "integrity": "sha512-LJhwaKf6XHwk2LQ5SdwoGNODoA8lRwks9bbEeAqqMf4e3hsrT7pZtX6MaHKYNFZKxF14JjI/VR+VRjGvxmaQoA==", "requires": { "cli-truncate": "^2.1.0", - "electron-dl": "^3.1.0", - "electron-is-dev": "^1.2.0" + "electron-dl": "^3.2.1", + "electron-is-dev": "^2.0.0" } }, "electron-devtools-installer": { @@ -11293,9 +11293,9 @@ } }, "electron-is-dev": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz", - "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz", + "integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==" }, "electron-log": { "version": "4.4.3", diff --git a/packages/insomnia-app/package.json b/packages/insomnia-app/package.json index fca122d43..c44604de6 100644 --- a/packages/insomnia-app/package.json +++ b/packages/insomnia-app/package.json @@ -91,7 +91,7 @@ "codemirror": "^5.62.3", "codemirror-graphql": "^1.0.2", "color": "^3.1.2", - "electron-context-menu": "^2.5.2", + "electron-context-menu": "^3.1.1", "electron-log": "^4.4.3", "electron-updater": "^4.6.1", "framer-motion": "^1.11.1", diff --git a/packages/insomnia-app/webpack/webpack.config.electron.ts b/packages/insomnia-app/webpack/webpack.config.electron.ts index 55633e953..b95791ecc 100644 --- a/packages/insomnia-app/webpack/webpack.config.electron.ts +++ b/packages/insomnia-app/webpack/webpack.config.electron.ts @@ -30,7 +30,7 @@ if (process.env.NODE_ENV === 'development') { plugins = productionConfig.plugins; } -const configuration: Configuration = { +const configuration: Configuration[] = [{ ...productionConfig, devtool, entry: ['./main.development.ts'], @@ -40,6 +40,14 @@ const configuration: Configuration = { }, target: 'electron-main', plugins, -}; +}, +{ + entry: './app/preload.js', + target: 'electron-preload', + output: { + path: path.join(__dirname, '../build'), + filename: 'preload.js', + }, +}]; export default configuration;