From abfabd41cf8c8db0cd67a665511bcd1e81041faa Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Fri, 7 Jul 2023 21:30:12 +0200 Subject: [PATCH] Chore/node-integration-continued (#6106) * don't import electron * add clipboard to preload * remove datadir helper * clean up helpers * remove electron-helpers * revert db path * delete test * more helpers * fix tests * fix tests * lint * fix inso --- packages/insomnia/.eslintrc.js | 2 +- packages/insomnia/src/__jest__/setup.ts | 30 ------------- .../common/__tests__/electron-helpers.test.ts | 26 ----------- packages/insomnia/src/common/constants.ts | 3 +- packages/insomnia/src/common/database.ts | 3 +- .../insomnia/src/common/electron-helpers.ts | 44 ------------------- packages/insomnia/src/global.d.ts | 1 + packages/insomnia/src/main.development.ts | 12 ++++- packages/insomnia/src/main/install-plugin.ts | 5 +-- packages/insomnia/src/main/ipc/electron.ts | 14 +++++- packages/insomnia/src/main/ipc/main.ts | 12 ++++- packages/insomnia/src/main/window-utils.ts | 27 +++++++++--- .../src/models/__tests__/response.test.ts | 4 +- .../models/helpers/__tests__/settings.test.ts | 23 ---------- .../insomnia/src/models/helpers/settings.ts | 6 +-- packages/insomnia/src/network/network.ts | 4 +- .../src/plugins/context/__tests__/app.test.ts | 44 ------------------- .../context/__tests__/response.test.ts | 4 +- packages/insomnia/src/plugins/context/app.tsx | 7 ++- packages/insomnia/src/plugins/create.ts | 5 +-- packages/insomnia/src/plugins/index.ts | 4 +- packages/insomnia/src/preload.ts | 8 ++++ .../src/ui/components/account-toolbar.tsx | 3 +- .../src/ui/components/base/copy-button.tsx | 3 +- .../insomnia/src/ui/components/base/link.tsx | 4 +- .../dropdowns/request-actions-dropdown.tsx | 3 +- .../editors/body/graph-ql-editor.tsx | 2 +- .../src/ui/components/markdown-preview.tsx | 3 +- .../src/ui/components/modals/login-modal.tsx | 8 ++-- .../src/ui/components/panes/response-pane.tsx | 3 +- .../src/ui/components/settings/plugins.tsx | 5 +-- .../viewers/response-multipart-viewer.tsx | 2 +- .../viewers/response-timeline-viewer.tsx | 3 +- .../ui/components/viewers/response-viewer.tsx | 3 +- .../ui/components/websockets/event-view.tsx | 3 +- .../src/ui/hooks/use-sync-migration.ts | 3 +- packages/insomnia/src/ui/hooks/use-vcs.ts | 3 +- packages/insomnia/src/ui/routes/import.tsx | 3 +- packages/insomnia/src/ui/routes/project.tsx | 3 +- packages/insomnia/src/ui/routes/root.tsx | 2 +- 40 files changed, 106 insertions(+), 241 deletions(-) delete mode 100644 packages/insomnia/src/common/__tests__/electron-helpers.test.ts delete mode 100644 packages/insomnia/src/common/electron-helpers.ts diff --git a/packages/insomnia/.eslintrc.js b/packages/insomnia/.eslintrc.js index 1ddc303886..dbdf7aa552 100644 --- a/packages/insomnia/.eslintrc.js +++ b/packages/insomnia/.eslintrc.js @@ -16,7 +16,7 @@ module.exports = { 'react/no-find-dom-node': OFF(UNKNOWN), 'no-restricted-properties': [ERROR, { property: 'openExternal', - message: 'use the `clickLink` function in `electron-helpers.ts` instead. see https://security.stackexchange.com/questions/225799/dangers-of-electrons-shell-openexternal-on-untrusted-content for more information.', + message: 'use the `window.main.openInBrowser` function instead. see https://security.stackexchange.com/questions/225799/dangers-of-electrons-shell-openexternal-on-untrusted-content for more information.', }], }, }; diff --git a/packages/insomnia/src/__jest__/setup.ts b/packages/insomnia/src/__jest__/setup.ts index 9e74db8831..698e7a5231 100644 --- a/packages/insomnia/src/__jest__/setup.ts +++ b/packages/insomnia/src/__jest__/setup.ts @@ -1,34 +1,4 @@ -const localStorageMock: Storage = (() => { - let store: Record = {}; - return { - get length() { - return Object.keys(store).length; - }, - - clear() { - store = {}; - }, - - getItem(key: string) { - return store[key]; - }, - - key() { - return null; - }, - - removeItem(key: string) { - delete store[key]; - }, - - setItem(key: string, value: string) { - store[key] = value.toString(); - }, - }; -})(); - globalThis.__DEV__ = false; -globalThis.localStorage = localStorageMock; globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => { process.nextTick(callback); diff --git a/packages/insomnia/src/common/__tests__/electron-helpers.test.ts b/packages/insomnia/src/common/__tests__/electron-helpers.test.ts deleted file mode 100644 index ecb269b71b..0000000000 --- a/packages/insomnia/src/common/__tests__/electron-helpers.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import electron from 'electron'; - -import { clickLink } from '../electron-helpers'; - -/* eslint-disable no-restricted-properties */ -describe('clickLink', () => { - it('should allow http links', () => { - const url = 'http://mockbin.org'; - clickLink(url); - expect(electron.shell.openExternal).toHaveBeenCalledWith(url); - }); - - it('should allow https links', () => { - const url = 'https://mockbin.org'; - clickLink(url); - expect(electron.shell.openExternal).toHaveBeenCalledWith(url); - }); - - it('should not allow smb links', () => { - const url = 'file:///C:/windows/system32/calc.exe'; - clickLink(url); - expect(electron.shell.openExternal).not.toHaveBeenCalledWith(url); - }); -}); -/* eslint-enable no-restricted-properties */ diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 6645124968..3219a4bfec 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -1,6 +1,5 @@ import appConfig from '../../config/config.json'; import { version } from '../../package.json'; -import { getPortableExecutableDir } from './electron-helpers'; import { KeyCombination } from './settings'; const env = process['env']; @@ -50,7 +49,7 @@ export function updatesSupported() { } // Updates are not supported for Windows portable binaries - if (isWindows() && getPortableExecutableDir()) { + if (isWindows() && process.env['PORTABLE_EXECUTABLE_DIR']) { return false; } diff --git a/packages/insomnia/src/common/database.ts b/packages/insomnia/src/common/database.ts index 7785c3d886..e5d3db0db2 100644 --- a/packages/insomnia/src/common/database.ts +++ b/packages/insomnia/src/common/database.ts @@ -14,7 +14,6 @@ import * as models from '../models/index'; import { isSettings } from '../models/settings'; import type { Workspace } from '../models/workspace'; import { DB_PERSIST_INTERVAL } from './constants'; -import { getDataDirectory } from './electron-helpers'; import { generateId } from './misc'; export interface Query { @@ -669,7 +668,7 @@ const allTypes = () => Object.keys(db); function getDBFilePath(modelType: string) { // NOTE: Do not EVER change this. EVER! - return fsPath.join(getDataDirectory(), `insomnia.${modelType}.db`); + return fsPath.join(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), `insomnia.${modelType}.db`); } // ~~~~~~~~~~~~~~~~ // diff --git a/packages/insomnia/src/common/electron-helpers.ts b/packages/insomnia/src/common/electron-helpers.ts deleted file mode 100644 index 3dea20dad5..0000000000 --- a/packages/insomnia/src/common/electron-helpers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as electron from 'electron'; -import mkdirp from 'mkdirp'; -import { join } from 'path'; - -import { version } from '../../package.json'; - -export function clickLink(href: string) { - const { protocol } = new URL(href); - if (protocol === 'http:' || protocol === 'https:') { - // eslint-disable-next-line no-restricted-properties -- this is, other than tests, what _should be_ the one and only place in this project where this is called. - electron.shell.openExternal(href); - } -} - -/** - * This environment variable is added by electron-builder. - * see: https://www.electron.build/configuration/nsis.html#portable\ - */ -export const getPortableExecutableDir = () => process.env['PORTABLE_EXECUTABLE_DIR']; - -export function getDataDirectory() { - const { app } = process.type === 'renderer' ? window : electron; - return process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData'); -} - -export function getTempDir() { - // NOTE: Using a fairly unique name here because "insomnia" is a common word - const { app } = process.type === 'renderer' ? window : electron; - const dir = join(app.getPath('temp'), `insomnia_${version}`); - mkdirp.sync(dir); - return dir; -} - -/** - * 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 - * On macOS the OS spellchecker is used and therefore we do not download any dictionary files. - * This API is a no-op on macOS. - */ -export const disableSpellcheckerDownload = () => { - electron.session.defaultSession.setSpellCheckerDictionaryDownloadURL( - 'https://00.00/' - ); -}; diff --git a/packages/insomnia/src/global.d.ts b/packages/insomnia/src/global.d.ts index 3ab7fdddfd..a6d4cbcdd8 100644 --- a/packages/insomnia/src/global.d.ts +++ b/packages/insomnia/src/global.d.ts @@ -8,6 +8,7 @@ declare global { dialog: Pick; app: Pick; shell: Pick; + clipboard: Pick; } } diff --git a/packages/insomnia/src/main.development.ts b/packages/insomnia/src/main.development.ts index 1504ddb654..a2b352ce90 100644 --- a/packages/insomnia/src/main.development.ts +++ b/packages/insomnia/src/main.development.ts @@ -7,7 +7,6 @@ import path from 'path'; import appConfig from '../config/config.json'; import { changelogUrl, getAppVersion, isDevelopment, isMac } from './common/constants'; import { database } from './common/database'; -import { disableSpellcheckerDownload } from './common/electron-helpers'; import log, { initializeLogging } from './common/log'; import { validateInsomniaConfig } from './common/validate-insomnia-config'; import { registerElectronHandlers } from './main/ipc/electron'; @@ -72,6 +71,17 @@ app.on('ready', async () => { registergRPCHandlers(); registerWebSocketHandlers(); + /** + * 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 + * On macOS the OS spellchecker is used and therefore we do not download any dictionary files. + * This API is a no-op on macOS. + */ + const disableSpellcheckerDownload = () => { + electron.session.defaultSession.setSpellCheckerDictionaryDownloadURL( + 'https://00.00/' + ); + }; disableSpellcheckerDownload(); if (isDevelopment()) { diff --git a/packages/insomnia/src/main/install-plugin.ts b/packages/insomnia/src/main/install-plugin.ts index bc2137b1e6..115c2b9106 100644 --- a/packages/insomnia/src/main/install-plugin.ts +++ b/packages/insomnia/src/main/install-plugin.ts @@ -7,7 +7,6 @@ import mkdirp from 'mkdirp'; import path from 'path'; import { isDevelopment, isWindows } from '../common/constants'; -import { getDataDirectory, getTempDir } from '../common/electron-helpers'; const YARN_DEPRECATED_WARN = /(?warning)(?[^>:].+[>:])(?.+)/; @@ -51,7 +50,7 @@ export default async function(lookupName: string) { info = await _isInsomniaPlugin(lookupName); // Get actual module name without version suffixes and things const moduleName = info.name; - const pluginDir = path.join(getDataDirectory(), 'plugins', moduleName); + const pluginDir = path.join(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'plugins', moduleName); // Make plugin directory mkdirp.sync(pluginDir); @@ -166,7 +165,7 @@ async function _isInsomniaPlugin(lookupName: string) { async function _installPluginToTmpDir(lookupName: string) { return new Promise<{ tmpDir: string }>((resolve, reject) => { - const tmpDir = path.join(getTempDir(), `${lookupName}-${Date.now()}`); + const tmpDir = path.join(electron.app.getPath('temp'), `${lookupName}-${Date.now()}`); mkdirp.sync(tmpDir); console.log(`[plugins] Installing plugin to ${tmpDir}`); childProcess.execFile( diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index ec0cc2ca2a..036c99a93d 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -1,5 +1,5 @@ import type { OpenDialogOptions, SaveDialogOptions } from 'electron'; -import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'; +import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from 'electron'; export function registerElectronHandlers() { ipcMain.on('setMenuBarVisibility', (_, visible: boolean) => { @@ -26,6 +26,18 @@ export function registerElectronHandlers() { shell.showItemInFolder(name); }); + ipcMain.on('readText', event => { + event.returnValue = clipboard.readText(); + }); + + ipcMain.on('writeText', (_, text: string) => { + clipboard.writeText(text); + }); + + ipcMain.on('clear', () => { + clipboard.clear(); + }); + ipcMain.on('getPath', (event, name: Parameters[0]) => { event.returnValue = app.getPath(name); }); diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index 81d0f12506..ab71e40a03 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -4,7 +4,7 @@ import { Spectral } from '@stoplight/spectral-core'; // @ts-expect-error - This is a bundled file not sure why it's not found import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader'; import { oas } from '@stoplight/spectral-rulesets'; -import { app, BrowserWindow, ipcMain, IpcRendererEvent } from 'electron'; +import { app, BrowserWindow, ipcMain, IpcRendererEvent, shell } from 'electron'; import fs from 'fs'; import { axiosRequest } from '../../network/axios-request'; @@ -18,6 +18,7 @@ import { gRPCBridgeAPI } from './grpc'; export interface MainBridgeAPI { loginStateChange: () => void; + openInBrowser: (url: string) => void; restart: () => void; halfSecondAfterAppStart: () => void; manualUpdateCheck: () => void; @@ -78,11 +79,20 @@ export function registerMainHandlers() { ipcMain.handle('installPlugin', (_, lookupName: string) => { return installPlugin(lookupName); }); + ipcMain.on('restart', () => { app.relaunch(); app.exit(); }); + ipcMain.on('openInBrowser', (_, href: string) => { + const { protocol } = new URL(href); + if (protocol === 'http:' || protocol === 'https:') { + // eslint-disable-next-line no-restricted-properties + shell.openExternal(href); + } + }); + ipcMain.handle('spectralRun', async (_, { contents, rulesetPath }: { contents: string; rulesetPath?: string; diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 7d68c05199..ab0aa27c4d 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -15,7 +15,6 @@ import { MNEMONIC_SYM, } from '../common/constants'; import { docsBase } from '../common/documentation'; -import { clickLink, getDataDirectory } from '../common/electron-helpers'; import * as log from '../common/log'; import LocalStorage from './local-storage'; @@ -112,7 +111,11 @@ export function createWindow() { console.log('[app] Navigate to ' + url); event.preventDefault(); - clickLink(url); + const { protocol } = new URL(url); + if (protocol === 'http:' || protocol === 'https:') { + // eslint-disable-next-line no-restricted-properties + shell.openExternal(url); + } }); newWindow?.webContents.setWindowOpenHandler(() => { @@ -153,7 +156,12 @@ export function createWindow() { return; } - clickLink(changelogUrl()); + const href = changelogUrl(); + const { protocol } = new URL(href); + if (protocol === 'http:' || protocol === 'https:') { + // eslint-disable-next-line no-restricted-properties + shell.openExternal(href); + } }, }, { @@ -335,7 +343,11 @@ export function createWindow() { label: `${MNEMONIC_SYM}Help and Support`, ...(isMac() ? {} : { accelerator: 'F1' }), click: () => { - clickLink(docsBase); + const { protocol } = new URL(docsBase); + if (protocol === 'http:' || protocol === 'https:') { + // eslint-disable-next-line no-restricted-properties + shell.openExternal(docsBase); + } }, }, { @@ -355,7 +367,7 @@ export function createWindow() { { label: `Show App ${MNEMONIC_SYM}Data Folder`, click: () => { - const directory = getDataDirectory(); + const directory = process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'); shell.showItemInFolder(directory); }, }, @@ -379,7 +391,8 @@ export function createWindow() { { label: 'Show Software License', click: () => { - clickLink('https://insomnia.rest/license'); + // eslint-disable-next-line no-restricted-properties + shell.openExternal('https://insomnia.rest/license'); }, }, ], @@ -633,7 +646,7 @@ export const setZoom = (transformer: (current: number) => number) => () => { }; function initLocalStorage() { - const localStoragePath = path.join(getDataDirectory(), 'localStorage'); + const localStoragePath = path.join(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'localStorage'); localStorage = new LocalStorage(localStoragePath); } diff --git a/packages/insomnia/src/models/__tests__/response.test.ts b/packages/insomnia/src/models/__tests__/response.test.ts index a5be9917b8..f3e7ade681 100644 --- a/packages/insomnia/src/models/__tests__/response.test.ts +++ b/packages/insomnia/src/models/__tests__/response.test.ts @@ -1,17 +1,17 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; +import electron from 'electron'; import fs from 'fs'; import path from 'path'; import zlib from 'zlib'; import { globalBeforeEach } from '../../__jest__/before-each'; -import { getDataDirectory } from '../../common/electron-helpers'; import * as models from '../../models'; describe('migrate()', () => { beforeEach(globalBeforeEach); it('does it', async () => { - const bodyPath = path.join(getDataDirectory(), 'foo.zip'); + const bodyPath = path.join(electron.app.getPath('userData'), 'foo.zip'); fs.writeFileSync(bodyPath, zlib.gzipSync('Hello World!')); const response = await models.initModel(models.response.type, { bodyPath, diff --git a/packages/insomnia/src/models/helpers/__tests__/settings.test.ts b/packages/insomnia/src/models/helpers/__tests__/settings.test.ts index 4d675859e6..51948c0292 100644 --- a/packages/insomnia/src/models/helpers/__tests__/settings.test.ts +++ b/packages/insomnia/src/models/helpers/__tests__/settings.test.ts @@ -2,7 +2,6 @@ import { afterAll, beforeEach, describe, expect, it, jest } from '@jest/globals' import { mocked } from 'jest-mock'; import * as _constants from '../../../common/constants'; -import * as electronHelpers from '../../../common/electron-helpers'; import { Settings } from '../../../common/settings'; import * as models from '../../../models'; import * as settingsHelpers from '../settings'; @@ -42,51 +41,29 @@ describe('getConfigFile', () => { }); afterAll(jest.resetAllMocks); - it('prioritizes portable config location over all others', () => { - jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue('portableExecutable'); - jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue('insomniaDataDirectory'); jest.spyOn(settingsHelpers, 'getLocalDevConfigFilePath').mockReturnValue('localDev'); const result = getConfigFile(); - expect(result.configPath).toContain('portableExecutable'); expect(result.configPath).toContain('insomnia.config.json'); }); it('prioritizes insomnia data directory over local dev when portable config is not found', () => { - jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue(undefined); - jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue('insomniaDataDirectory'); jest.spyOn(settingsHelpers, 'getLocalDevConfigFilePath').mockReturnValue('localDev'); const result = getConfigFile(); - expect(result.configPath).toContain('insomniaDataDirectory'); expect(result.configPath).toContain('insomnia.config.json'); }); it('returns the local dev config file if no others are found', () => { - jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue(undefined); - // @ts-expect-error intentionally invalid to simulate the file not being found - jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue(undefined); jest.spyOn(settingsHelpers, 'getLocalDevConfigFilePath').mockReturnValue('localDev'); const result = getConfigFile(); - expect(result.configPath).toContain('localDev'); expect(result.configPath).toContain('insomnia.config.json'); }); - - it('returns an internal fallback if no configs are found (in production mode)', () => { - isDevelopment.mockReturnValue(false); - jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue(undefined); - // @ts-expect-error intentionally invalid to simulate the file not being found - jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue(undefined); - - const result = getConfigFile(); - - expect(result).toMatchObject({ configPath: '' }); - }); }); describe('getControlledStatus', () => { diff --git a/packages/insomnia/src/models/helpers/settings.ts b/packages/insomnia/src/models/helpers/settings.ts index 16d3fdc5dd..71563ee863 100644 --- a/packages/insomnia/src/models/helpers/settings.ts +++ b/packages/insomnia/src/models/helpers/settings.ts @@ -1,9 +1,9 @@ +import electron from 'electron'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import type { ValueOf } from 'type-fest'; import { isDevelopment } from '../../common/constants'; -import { getDataDirectory, getPortableExecutableDir } from '../../common/electron-helpers'; import { Settings } from '../../common/settings'; import { INSOMNIA_CONFIG_FILENAME, InsomniaConfig } from '../../utils/config/entities'; import { ErrorResult, isErrorResult, validate } from '../../utils/config/validate'; @@ -68,8 +68,8 @@ const addConfigFileToPath = (path: string | undefined) => ( ); export const getConfigFile = () => { - const portableExecutable = getPortableExecutableDir(); - const insomniaDataDirectory = getDataDirectory(); + const portableExecutable = process.env['PORTABLE_EXECUTABLE_DIR']; + const insomniaDataDirectory = process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'); const localDev = getLocalDevConfigFilePath(); const configPaths = [ portableExecutable, diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index deaa1c9e86..eba47f4c2c 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -1,4 +1,5 @@ import clone from 'clone'; +import electron from 'electron'; import fs from 'fs'; import mkdirp from 'mkdirp'; import { join as pathJoin } from 'path'; @@ -6,7 +7,6 @@ import { v4 as uuidv4 } from 'uuid'; import { cookiesFromJar, jarFromCookies } from '../common/cookies'; import { database as db } from '../common/database'; -import { getDataDirectory } from '../common/electron-helpers'; import { getContentTypeHeader, getLocationHeader, @@ -387,7 +387,7 @@ async function _applyResponsePluginHooks( export function storeTimeline(timeline: ResponseTimelineEntry[]): Promise { const timelineStr = JSON.stringify(timeline, null, '\t'); const timelineHash = uuidv4(); - const responsesDir = pathJoin(getDataDirectory(), 'responses'); + const responsesDir = pathJoin(process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : electron).app.getPath('userData'), 'responses'); mkdirp.sync(responsesDir); const timelinePath = pathJoin(responsesDir, timelineHash + '.timeline'); if (process.type === 'renderer') { diff --git a/packages/insomnia/src/plugins/context/__tests__/app.test.ts b/packages/insomnia/src/plugins/context/__tests__/app.test.ts index 2364fe4239..bf1c92f9e1 100644 --- a/packages/insomnia/src/plugins/context/__tests__/app.test.ts +++ b/packages/insomnia/src/plugins/context/__tests__/app.test.ts @@ -1,6 +1,4 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals'; -import electron from 'electron'; -import { mocked } from 'jest-mock'; import appPackageJson from '../../../../package.json'; import { globalBeforeEach } from '../../../__jest__/before-each'; @@ -118,45 +116,3 @@ describe('app.getInfo()', () => { }); }); - -describe('app.clipboard', () => { - it('writes to clipboard', () => { - // Arrange - const mockedClipboard = mocked(electron.clipboard); - const context = plugin.init(); - const text = 'abc'; - - // Act - context.app.clipboard.writeText(text); - - // Assert - expect(mockedClipboard.writeText).toHaveBeenCalledWith(text); - }); - - it('reads from clipboard', () => { - // Arrange - const text = 'abc'; - const mockedClipboard = mocked(electron.clipboard); - mockedClipboard.readText.mockReturnValue(text); - const context = plugin.init(); - - // Act - const outputText = context.app.clipboard.readText(); - - // Assert - expect(outputText).toBe(text); - expect(mockedClipboard.readText).toHaveBeenCalled(); - }); - - it('clears clipboard', () => { - // Arrange - const mockedClipboard = mocked(electron.clipboard); - const context = plugin.init(); - - // Act - context.app.clipboard.clear(); - - // Assert - expect(mockedClipboard.clear).toHaveBeenCalled(); - }); -}); diff --git a/packages/insomnia/src/plugins/context/__tests__/response.test.ts b/packages/insomnia/src/plugins/context/__tests__/response.test.ts index 31e02a15c0..b2a5e6de0c 100644 --- a/packages/insomnia/src/plugins/context/__tests__/response.test.ts +++ b/packages/insomnia/src/plugins/context/__tests__/response.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; +import electron from 'electron'; import fs from 'fs'; import path from 'path'; import { globalBeforeEach } from '../../../__jest__/before-each'; -import { getTempDir } from '../../../common/electron-helpers'; import * as models from '../../../models/index'; import * as plugin from '../response'; @@ -37,7 +37,7 @@ describe('response.*', () => { beforeEach(globalBeforeEach); it('works for basic and full response', async () => { - const bodyPath = path.join(getTempDir(), 'response.zip'); + const bodyPath = path.join(electron.app.getPath('temp'), 'response.zip'); fs.writeFileSync(bodyPath, Buffer.from('Hello World!')); const response = await models.initModel(models.response.type, { bodyPath, diff --git a/packages/insomnia/src/plugins/context/app.tsx b/packages/insomnia/src/plugins/context/app.tsx index b136cfa513..8a66a58298 100644 --- a/packages/insomnia/src/plugins/context/app.tsx +++ b/packages/insomnia/src/plugins/context/app.tsx @@ -1,4 +1,3 @@ -import * as electron from 'electron'; import React from 'react'; import type ReactDOM from 'react-dom'; @@ -180,15 +179,15 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { clipboard: { readText() { - return electron.clipboard.readText(); + return window.clipboard.readText(); }, writeText(text) { - electron.clipboard.writeText(text); + window.clipboard.writeText(text); }, clear() { - electron.clipboard.clear(); + window.clipboard.clear(); }, }, diff --git a/packages/insomnia/src/plugins/create.ts b/packages/insomnia/src/plugins/create.ts index 267760b073..486f67b2e1 100644 --- a/packages/insomnia/src/plugins/create.ts +++ b/packages/insomnia/src/plugins/create.ts @@ -1,15 +1,14 @@ +import electron from 'electron'; import fs from 'fs'; import mkdirp from 'mkdirp'; import path from 'path'; -import { getDataDirectory } from '../common/electron-helpers'; - export async function createPlugin( moduleName: string, version: string, mainJs: string, ) { - const pluginDir = path.join(getDataDirectory(), 'plugins', moduleName); + const pluginDir = path.join(process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : electron).app.getPath('userData'), 'plugins', moduleName); if (fs.existsSync(pluginDir)) { throw new Error(`Plugin already exists at "${pluginDir}"`); diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 79b3baa127..10e2d0c495 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -1,10 +1,10 @@ +import electron from 'electron'; import fs from 'fs'; import mkdirp from 'mkdirp'; import path from 'path'; import appConfig from '../../config/config.json'; import { ParsedApiSpec } from '../common/api-specs'; -import { getDataDirectory } from '../common/electron-helpers'; import { resolveHomePath } from '../common/misc'; import type { PluginConfig, PluginConfigMap } from '../common/settings'; import * as models from '../models'; @@ -209,7 +209,7 @@ export async function getPlugins(force = false): Promise { .filter(p => p) .map(resolveHomePath); // Make sure the default directories exist - const pluginPath = path.join(getDataDirectory(), 'plugins'); + const pluginPath = path.join(process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : electron).app.getPath('userData'), 'plugins'); mkdirp.sync(pluginPath); // Also look in node_modules folder in each directory const basePaths = [pluginPath, ...extraPaths]; diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index e76ed22fd7..f411516eaf 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -27,6 +27,7 @@ const grpc: gRPCBridgeAPI = { const main: Window['main'] = { loginStateChange: () => ipcRenderer.send('loginStateChange'), restart: () => ipcRenderer.send('restart'), + openInBrowser: options => ipcRenderer.send('openInBrowser', options), halfSecondAfterAppStart: () => ipcRenderer.send('halfSecondAfterAppStart'), manualUpdateCheck: () => ipcRenderer.send('manualUpdateCheck'), exportAllWorkspaces: () => ipcRenderer.invoke('exportAllWorkspaces'), @@ -57,15 +58,22 @@ const app: Window['app'] = { const shell: Window['shell'] = { showItemInFolder: options => ipcRenderer.send('showItemInFolder', options), }; +const clipboard: Window['clipboard'] = { + readText: () => ipcRenderer.sendSync('readText'), + writeText: options => ipcRenderer.send('writeText', options), + clear: () => ipcRenderer.send('clear'), +}; if (process.contextIsolated) { contextBridge.exposeInMainWorld('main', main); contextBridge.exposeInMainWorld('dialog', dialog); contextBridge.exposeInMainWorld('app', app); contextBridge.exposeInMainWorld('shell', shell); + contextBridge.exposeInMainWorld('clipboard', clipboard); } else { window.main = main; window.dialog = dialog; window.app = app; window.shell = shell; + window.clipboard = clipboard; } diff --git a/packages/insomnia/src/ui/components/account-toolbar.tsx b/packages/insomnia/src/ui/components/account-toolbar.tsx index 8356bf8b45..f007e68c9c 100644 --- a/packages/insomnia/src/ui/components/account-toolbar.tsx +++ b/packages/insomnia/src/ui/components/account-toolbar.tsx @@ -2,7 +2,6 @@ import React, { Fragment } from 'react'; import styled from 'styled-components'; import * as session from '../../account/session'; -import { clickLink } from '../../common/electron-helpers'; import { Dropdown, DropdownButton, DropdownItem, ItemContent } from './base/dropdown'; import { Link as ExternalLink } from './base/link'; import { showLoginModal } from './modals/login-modal'; @@ -53,7 +52,7 @@ export const AccountToolbar = () => { icon="gear" label='Account Settings' stayOpenAfterClick - onClick={() => clickLink('https://app.insomnia.rest/app/account/')} + onClick={() => window.main.openInBrowser('https://app.insomnia.rest/app/account/')} /> = ({ const toCopy = typeof content === 'string' ? content : await content(); if (toCopy) { - clipboard.writeText(toCopy); + window.clipboard.writeText(toCopy); } setshowConfirmation(true); }, [content]); diff --git a/packages/insomnia/src/ui/components/base/link.tsx b/packages/insomnia/src/ui/components/base/link.tsx index 2c1a65694b..18f8c598f7 100644 --- a/packages/insomnia/src/ui/components/base/link.tsx +++ b/packages/insomnia/src/ui/components/base/link.tsx @@ -1,8 +1,6 @@ import classnames from 'classnames'; import React, { FC, ReactNode, useCallback } from 'react'; -import { clickLink } from '../../../common/electron-helpers'; - interface Props { href: string; title?: string; @@ -25,7 +23,7 @@ export const Link: FC = ({ const handleClick = useCallback((event: React.MouseEvent) => { event?.preventDefault(); onClick?.(event); // Also call onClick that was passed to us if there was one - clickLink(href); + window.main.openInBrowser(href); }, [onClick, href]); if (button) { diff --git a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx index 0c051ac197..0b583cb788 100644 --- a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx @@ -1,4 +1,3 @@ -import { clipboard } from 'electron'; import HTTPSnippet from 'httpsnippet'; import React, { forwardRef, useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -97,7 +96,7 @@ export const RequestActionsDropdown = forwardRef(({ const cmd = snippet.convert('shell', 'curl'); if (cmd) { - clipboard.writeText(cmd); + window.clipboard.writeText(cmd); } } catch (err) { showModal(AlertModal, { diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index 16facae45d..f0901ed4bb 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -1,7 +1,7 @@ import { LintOptions, ShowHintOptions, TextMarker } from 'codemirror'; import { GraphQLInfoOptions } from 'codemirror-graphql/info'; import { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; -import { OpenDialogOptions } from 'electron'; +import type { OpenDialogOptions } from 'electron'; import { readFileSync } from 'fs'; import { DefinitionNode, DocumentNode, GraphQLNonNull, GraphQLSchema, Kind, NonNullTypeNode, OperationDefinitionNode, parse, typeFromAST } from 'graphql'; import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities'; diff --git a/packages/insomnia/src/ui/components/markdown-preview.tsx b/packages/insomnia/src/ui/components/markdown-preview.tsx index 59fe8af3c7..fea4e5a5f1 100644 --- a/packages/insomnia/src/ui/components/markdown-preview.tsx +++ b/packages/insomnia/src/ui/components/markdown-preview.tsx @@ -2,7 +2,6 @@ import classnames from 'classnames'; import highlight from 'highlight.js/lib/common'; import React, { FC, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { clickLink } from '../../common/electron-helpers'; import { markdownToHTML } from '../../common/markdown-to-html'; import { HandleRender } from '../../common/render'; import { useGatedNunjucks } from '../context/nunjucks/use-gated-nunjucks'; @@ -55,7 +54,7 @@ export const MarkdownPreview: FC = ({ markdown, className, heading }) => }, [compiled]); const _handleClickLink = (event: any) => { event.preventDefault(); - clickLink(event.target.getAttribute('href')); + window.main.openInBrowser(event.target.getAttribute('href')); }; return ( diff --git a/packages/insomnia/src/ui/components/modals/login-modal.tsx b/packages/insomnia/src/ui/components/modals/login-modal.tsx index 1a82a4c92e..71772df64d 100644 --- a/packages/insomnia/src/ui/components/modals/login-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/login-modal.tsx @@ -1,8 +1,6 @@ -import { clipboard } from 'electron'; import React, { FormEvent, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import * as session from '../../../account/session'; -import { clickLink } from '../../../common/electron-helpers'; import { getLoginUrl, submitAuthCode } from '../../auth-session-provider'; import { type ModalHandle, Modal } from '../base/modal'; import { ModalBody } from '../base/modal-body'; @@ -68,7 +66,7 @@ export const LoginModal = forwardRef(function LoginModal({ modalRef.current?.show(); if (!reauth) { - clickLink(url); + window.main.openInBrowser(url); } }, }), []); @@ -91,11 +89,11 @@ export const LoginModal = forwardRef(function LoginModal({ }, []); const copyUrl = useCallback(() => { - clipboard.writeText(state.url); + window.clipboard.writeText(state.url); }, [state.url]); const openUrl = useCallback(() => { - clickLink(state.url); + window.main.openInBrowser(state.url); }, [state.url]); const renderBody = () => { diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index 5219ec9b12..733f68564e 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -1,4 +1,3 @@ -import { clipboard } from 'electron'; import fs from 'fs'; import { extension as mimeExtension } from 'mime-types'; import React, { FC, useCallback } from 'react'; @@ -70,7 +69,7 @@ export const ResponsePane: FC = ({ const handleCopyResponseToClipboard = useCallback(async () => { const bodyBuffer = handleGetResponseBody(); if (bodyBuffer) { - clipboard.writeText(bodyBuffer.toString('utf8')); + window.clipboard.writeText(bodyBuffer.toString('utf8')); } }, [handleGetResponseBody]); const handleDownloadResponseBody = useCallback(async (prettify: boolean) => { diff --git a/packages/insomnia/src/ui/components/settings/plugins.tsx b/packages/insomnia/src/ui/components/settings/plugins.tsx index ccc7587c8f..4ebdb0e338 100644 --- a/packages/insomnia/src/ui/components/settings/plugins.tsx +++ b/packages/insomnia/src/ui/components/settings/plugins.tsx @@ -7,7 +7,6 @@ import { PLUGIN_HUB_BASE, } from '../../../common/constants'; import { docsPlugins } from '../../../common/documentation'; -import { clickLink, getDataDirectory } from '../../../common/electron-helpers'; import * as models from '../../../models'; import { createPlugin } from '../../../plugins/create'; import type { Plugin } from '../../../plugins/index'; @@ -206,7 +205,7 @@ export const Plugins: FC = () => {
@@ -252,7 +251,7 @@ export const Plugins: FC = () => { style={{ marginLeft: '0.3em', }} - onClick={() => window.shell.showItemInFolder(path.join(getDataDirectory(), 'plugins'))} + onClick={() => window.shell.showItemInFolder(path.join(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'), 'plugins'))} > Reveal Plugins Folder diff --git a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx index 0b69cd251c..14a41b0954 100644 --- a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx @@ -1,5 +1,5 @@ import { format } from 'date-fns'; -import { SaveDialogOptions } from 'electron'; +import type { SaveDialogOptions } from 'electron'; import fs from 'fs'; import { extension as mimeExtension } from 'mime-types'; import multiparty from 'multiparty'; diff --git a/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx index 916dc9239f..d2040a0541 100644 --- a/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx @@ -1,6 +1,5 @@ import React, { FC, useEffect, useRef } from 'react'; -import { clickLink } from '../../../common/electron-helpers'; import type { ResponseTimelineEntry } from '../../../main/network/libcurl-promise'; import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor'; @@ -46,7 +45,7 @@ export const ResponseTimelineViewer: FC = ({ timeline }) => { ref={editorRef} hideLineNumbers readOnly - onClickLink={clickLink} + onClickLink={window.main.openInBrowser} defaultValue={rows} className="pad-left" mode="curl" diff --git a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx index 36144701b8..92fbf2e66e 100644 --- a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx @@ -11,7 +11,6 @@ import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW, } from '../../../common/constants'; -import { clickLink } from '../../../common/electron-helpers'; import { xmlDecode } from '../../../common/misc'; import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor'; import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; @@ -347,7 +346,7 @@ export const ResponseViewer = ({ filterHistory={filterHistory} mode={getBodyAsString()?.match(/^\s*<\?xml [^?]*\?>/) ? 'application/xml' : _getContentType()} noMatchBrackets - onClickLink={url => !disablePreviewLinks && clickLink(getBodyAsString()?.match(/^\s*<\?xml [^?]*\?>/) ? xmlDecode(url) : url)} + onClickLink={url => !disablePreviewLinks && window.main.openInBrowser(getBodyAsString()?.match(/^\s*<\?xml [^?]*\?>/) ? xmlDecode(url) : url)} placeholder="..." readOnly uniquenessKey={responseId} diff --git a/packages/insomnia/src/ui/components/websockets/event-view.tsx b/packages/insomnia/src/ui/components/websockets/event-view.tsx index 416baefb16..88cfeaca39 100644 --- a/packages/insomnia/src/ui/components/websockets/event-view.tsx +++ b/packages/insomnia/src/ui/components/websockets/event-view.tsx @@ -1,4 +1,3 @@ -import { clipboard } from 'electron'; import fs from 'fs'; import React, { FC, useCallback } from 'react'; import { useSelector } from 'react-redux'; @@ -78,7 +77,7 @@ export const MessageEventView: FC> = ({ event, requ }, [raw]); const handleCopyResponseToClipboard = useCallback(() => { - clipboard.writeText(raw); + window.clipboard.writeText(raw); }, [raw]); const previewMode = useSelector(selectResponsePreviewMode); diff --git a/packages/insomnia/src/ui/hooks/use-sync-migration.ts b/packages/insomnia/src/ui/hooks/use-sync-migration.ts index 411444f650..240aac76cf 100644 --- a/packages/insomnia/src/ui/hooks/use-sync-migration.ts +++ b/packages/insomnia/src/ui/hooks/use-sync-migration.ts @@ -1,13 +1,12 @@ import { useAsync } from 'react-use'; import { onLoginLogout } from '../../account/session'; -import { getDataDirectory } from '../../common/electron-helpers'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import { migrateCollectionsIntoRemoteProject } from '../../sync/vcs/migrate-collections'; import { VCS } from '../../sync/vcs/vcs'; const check = async () => { - const driver = FileSystemDriver.create(getDataDirectory()); + const driver = FileSystemDriver.create(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData')); await migrateCollectionsIntoRemoteProject(new VCS(driver)); }; diff --git a/packages/insomnia/src/ui/hooks/use-vcs.ts b/packages/insomnia/src/ui/hooks/use-vcs.ts index 91d4d0ab89..9c73cd265f 100644 --- a/packages/insomnia/src/ui/hooks/use-vcs.ts +++ b/packages/insomnia/src/ui/hooks/use-vcs.ts @@ -1,6 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { getDataDirectory } from '../../common/electron-helpers'; import { generateId } from '../../common/misc'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import { type MergeConflict } from '../../sync/types'; @@ -27,7 +26,7 @@ export function useVCS({ async function updateVCS() { let vcsInstance = getVCS(); if (!vcsInstance) { - const driver = FileSystemDriver.create(getDataDirectory()); + const driver = FileSystemDriver.create(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData')); vcsInstance = await initVCS(driver, async conflicts => { return new Promise(resolve => { diff --git a/packages/insomnia/src/ui/routes/import.tsx b/packages/insomnia/src/ui/routes/import.tsx index e8ec768aa3..dbe3fdc500 100644 --- a/packages/insomnia/src/ui/routes/import.tsx +++ b/packages/insomnia/src/ui/routes/import.tsx @@ -1,5 +1,4 @@ // Import -import { clipboard } from 'electron'; import { ActionFunction, redirect } from 'react-router-dom'; import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../../common/constants'; @@ -42,7 +41,7 @@ export const scanForResourcesAction: ActionFunction = async ({ request }): Promi uri, }); } else { - content = clipboard.readText(); + content = window.clipboard.readText(); } if (!content) { diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index 95b43ff134..6550d287ab 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -30,7 +30,6 @@ import { ACTIVITY_SPEC, DashboardSortOrder, } from '../../common/constants'; -import { clickLink } from '../../common/electron-helpers'; import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc'; import { descendingNumberSort, sortMethodMap } from '../../common/sorting'; import { strings } from '../../common/strings'; @@ -502,7 +501,7 @@ const OrganizationProjectsSidebar: FC<{ { - clickLink(key.toString()); + window.main.openInBrowser(key.toString()); }} >