mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-12-22 18:18:55 +00:00
Add validation to opening URLs in external desktop handler (fix #1459)
This commit is contained in:
parent
eecc33c1aa
commit
840fe4a199
@ -18,7 +18,7 @@ export function initContextMenu(
|
|||||||
options: OutputOptions,
|
options: OutputOptions,
|
||||||
window?: BrowserWindow,
|
window?: BrowserWindow,
|
||||||
): void {
|
): void {
|
||||||
log.debug('initContextMenu', { options, window });
|
log.debug('initContextMenu');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
contextMenu({
|
contextMenu({
|
||||||
|
@ -45,7 +45,7 @@ export function createMenu(
|
|||||||
options: OutputOptions,
|
options: OutputOptions,
|
||||||
mainWindow: BrowserWindow,
|
mainWindow: BrowserWindow,
|
||||||
): void {
|
): void {
|
||||||
log.debug('createMenu', { options, mainWindow });
|
log.debug('createMenu', { options });
|
||||||
const menuTemplate = generateMenu(options, mainWindow);
|
const menuTemplate = generateMenu(options, mainWindow);
|
||||||
|
|
||||||
injectBookmarks(menuTemplate);
|
injectBookmarks(menuTemplate);
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
import { shell } from 'electron';
|
||||||
|
jest.mock('./windowHelpers');
|
||||||
|
|
||||||
import {
|
import {
|
||||||
linkIsInternal,
|
|
||||||
getCounterValue,
|
|
||||||
removeUserAgentSpecifics,
|
|
||||||
cleanupPlainText,
|
cleanupPlainText,
|
||||||
|
getCounterValue,
|
||||||
|
linkIsInternal,
|
||||||
|
openExternal,
|
||||||
|
removeUserAgentSpecifics,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
import { showNavigationBlockedMessage } from './windowHelpers';
|
||||||
|
|
||||||
const internalUrl = 'https://medium.com/';
|
const internalUrl = 'https://medium.com/';
|
||||||
const internalUrlWww = 'https://www.medium.com/';
|
const internalUrlWww = 'https://www.medium.com/';
|
||||||
@ -285,3 +290,59 @@ describe('cleanupPlainText', () => {
|
|||||||
expect(cleanupPlainText(' this is a test ')).toBe('this is a test');
|
expect(cleanupPlainText(' this is a test ')).toBe('this is a test');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('openExternal', () => {
|
||||||
|
const mockShellOpenExternal: jest.SpyInstance = jest.spyOn(
|
||||||
|
shell,
|
||||||
|
'openExternal',
|
||||||
|
);
|
||||||
|
const mockShowNavigationBlockedMessage: jest.SpyInstance =
|
||||||
|
showNavigationBlockedMessage as jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockShellOpenExternal.mockReset();
|
||||||
|
mockShowNavigationBlockedMessage
|
||||||
|
.mockReset()
|
||||||
|
.mockReturnValue(Promise.resolve(undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mockShellOpenExternal.mockRestore();
|
||||||
|
mockShowNavigationBlockedMessage.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('https urls scheme should *not* be blocked', async () => {
|
||||||
|
await openExternal('https://whatever.foo');
|
||||||
|
|
||||||
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
|
expect(mockShellOpenExternal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('urls with whitelisted scheme should *not* be blocked', async () => {
|
||||||
|
await openExternal('ircs://irc.libera.chat/whatever');
|
||||||
|
|
||||||
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
|
expect(mockShellOpenExternal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('urls with non-allowlisted scheme *should* be blocked', async () => {
|
||||||
|
await openExternal('barf://whatever.foo');
|
||||||
|
|
||||||
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockShellOpenExternal).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('potentially-malicious urls *should* be blocked', async () => {
|
||||||
|
await openExternal('https://hello.com/wor%00ld');
|
||||||
|
|
||||||
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockShellOpenExternal).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('malformed urls *should* be blocked', async () => {
|
||||||
|
await openExternal('zombocom');
|
||||||
|
|
||||||
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockShellOpenExternal).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -5,9 +5,80 @@ import * as path from 'path';
|
|||||||
import { BrowserWindow, OpenExternalOptions, shell } from 'electron';
|
import { BrowserWindow, OpenExternalOptions, shell } from 'electron';
|
||||||
|
|
||||||
import * as log from '../helpers/loggingHelper';
|
import * as log from '../helpers/loggingHelper';
|
||||||
|
import { showNavigationBlockedMessage } from './windowHelpers';
|
||||||
|
|
||||||
export const INJECT_DIR = path.join(__dirname, '..', 'inject');
|
export const INJECT_DIR = path.join(__dirname, '..', 'inject');
|
||||||
|
|
||||||
|
// Taken from Firefox's. Location might vary in codebase, search for one of them, e.g.
|
||||||
|
// https://searchfox.org/mozilla-central/search?q=%22xmpp%22&path=&case=false®exp=false
|
||||||
|
const SAFE_URL_PROTOCOLS_FIREFOX = [
|
||||||
|
'bitcoin:',
|
||||||
|
'ftp:',
|
||||||
|
'ftps:',
|
||||||
|
'geo:',
|
||||||
|
'im:',
|
||||||
|
'irc:',
|
||||||
|
'ircs:',
|
||||||
|
'magnet:',
|
||||||
|
'mailto:',
|
||||||
|
'matrix:',
|
||||||
|
'mms:',
|
||||||
|
'news:',
|
||||||
|
'nntp:',
|
||||||
|
'openpgp4fpr:',
|
||||||
|
'sftp:',
|
||||||
|
'sip:',
|
||||||
|
'sms:',
|
||||||
|
'smsto:',
|
||||||
|
'ssh:',
|
||||||
|
'tel:',
|
||||||
|
'urn:',
|
||||||
|
'webcal:',
|
||||||
|
'wtai:',
|
||||||
|
'xmpp:',
|
||||||
|
];
|
||||||
|
const SAFE_URL_PROTOCOLS = ['http:', 'https:', ...SAFE_URL_PROTOCOLS_FIREFOX];
|
||||||
|
const SHELL_SAFETY_FEEDBACK_STR =
|
||||||
|
'If you believe this URL should open, you might be right, and our validation might be excessive.' +
|
||||||
|
'Please share this error & URL at https://github.com/nativefier/nativefier/issues/1459';
|
||||||
|
|
||||||
|
export function isUrlShellSafe(
|
||||||
|
urlToGo: string,
|
||||||
|
): { blocked: false } | { blocked: true; reason: string } {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(urlToGo.toLowerCase());
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
reason: `URL appears malformed. ${SHELL_SAFETY_FEEDBACK_STR}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SAFE_URL_PROTOCOLS.includes(url.protocol)) {
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
reason: `URL protocol is disallowed. ${SHELL_SAFETY_FEEDBACK_STR}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://cwe.mitre.org/data/definitions/177.html
|
||||||
|
if (
|
||||||
|
urlToGo.includes('%00') ||
|
||||||
|
urlToGo.includes('%0a') ||
|
||||||
|
urlToGo.includes('%2e') ||
|
||||||
|
urlToGo.includes('%2f') ||
|
||||||
|
urlToGo.includes('%5c')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
reason: `URL might be malicious. ${SHELL_SAFETY_FEEDBACK_STR}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to print debug messages from the main process in the browser window
|
* Helper to print debug messages from the main process in the browser window
|
||||||
*/
|
*/
|
||||||
@ -166,11 +237,28 @@ export function nativeTabsSupported(): boolean {
|
|||||||
return isOSX();
|
return isOSX();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the given external protocol URL in the desktop's default manner
|
||||||
|
* (e.g. `mailto:` URLs in the user's default mail agent), with extra validation.
|
||||||
|
*/
|
||||||
export function openExternal(
|
export function openExternal(
|
||||||
url: string,
|
url: string,
|
||||||
options?: OpenExternalOptions,
|
options?: OpenExternalOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.debug('openExternal', { url, options });
|
const urlShellSafety = isUrlShellSafe(url);
|
||||||
|
log.debug('openExternal', { url, options, urlShellSafety });
|
||||||
|
if (urlShellSafety.blocked) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
showNavigationBlockedMessage(
|
||||||
|
`Navigation blocked to ${url}\n\n${urlShellSafety.reason}`,
|
||||||
|
)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return shell.openExternal(url, options);
|
return shell.openExternal(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ const {
|
|||||||
onWillPreventUnload: (event: unknown) => void;
|
onWillPreventUnload: (event: unknown) => void;
|
||||||
} = jest.requireActual('./windowEvents');
|
} = jest.requireActual('./windowEvents');
|
||||||
import {
|
import {
|
||||||
blockExternalURL,
|
showNavigationBlockedMessage,
|
||||||
createAboutBlankWindow,
|
createAboutBlankWindow,
|
||||||
createNewTab,
|
createNewTab,
|
||||||
} from './windowHelpers';
|
} from './windowHelpers';
|
||||||
@ -49,7 +49,8 @@ describe('onNewWindowHelper', () => {
|
|||||||
targetUrl: originalURL,
|
targetUrl: originalURL,
|
||||||
zoom: 1.0,
|
zoom: 1.0,
|
||||||
};
|
};
|
||||||
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
|
const mockShowNavigationBlockedMessage: jest.SpyInstance =
|
||||||
|
showNavigationBlockedMessage as jest.Mock;
|
||||||
const mockCreateAboutBlank: jest.SpyInstance =
|
const mockCreateAboutBlank: jest.SpyInstance =
|
||||||
createAboutBlankWindow as jest.Mock;
|
createAboutBlankWindow as jest.Mock;
|
||||||
const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock;
|
const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock;
|
||||||
@ -63,7 +64,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
const setupWindow = jest.fn();
|
const setupWindow = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockBlockExternalURL
|
mockShowNavigationBlockedMessage
|
||||||
.mockReset()
|
.mockReset()
|
||||||
.mockReturnValue(Promise.resolve(undefined));
|
.mockReturnValue(Promise.resolve(undefined));
|
||||||
mockCreateAboutBlank.mockReset();
|
mockCreateAboutBlank.mockReset();
|
||||||
@ -76,7 +77,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
mockBlockExternalURL.mockRestore();
|
mockShowNavigationBlockedMessage.mockRestore();
|
||||||
mockCreateAboutBlank.mockRestore();
|
mockCreateAboutBlank.mockRestore();
|
||||||
mockCreateNewTab.mockRestore();
|
mockCreateNewTab.mockRestore();
|
||||||
mockLinkIsInternal.mockRestore();
|
mockLinkIsInternal.mockRestore();
|
||||||
@ -95,7 +96,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -113,7 +114,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -134,7 +135,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -150,7 +151,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -168,7 +169,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -193,7 +194,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
true,
|
true,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -218,7 +219,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -234,7 +235,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -250,7 +251,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -266,7 +267,7 @@ describe('onNewWindowHelper', () => {
|
|||||||
|
|
||||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -277,13 +278,14 @@ describe('onWillNavigate', () => {
|
|||||||
const internalURL = 'https://medium.com/topics/technology';
|
const internalURL = 'https://medium.com/topics/technology';
|
||||||
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
|
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
|
||||||
|
|
||||||
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
|
const mockShowNavigationBlockedMessage: jest.SpyInstance =
|
||||||
|
showNavigationBlockedMessage as jest.Mock;
|
||||||
const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock;
|
const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock;
|
||||||
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
|
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
|
||||||
const preventDefault = jest.fn();
|
const preventDefault = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockBlockExternalURL
|
mockShowNavigationBlockedMessage
|
||||||
.mockReset()
|
.mockReset()
|
||||||
.mockReturnValue(Promise.resolve(undefined));
|
.mockReturnValue(Promise.resolve(undefined));
|
||||||
mockLinkIsInternal.mockReset().mockReturnValue(false);
|
mockLinkIsInternal.mockReset().mockReturnValue(false);
|
||||||
@ -292,7 +294,7 @@ describe('onWillNavigate', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
mockBlockExternalURL.mockRestore();
|
mockShowNavigationBlockedMessage.mockRestore();
|
||||||
mockLinkIsInternal.mockRestore();
|
mockLinkIsInternal.mockRestore();
|
||||||
mockOpenExternal.mockRestore();
|
mockOpenExternal.mockRestore();
|
||||||
});
|
});
|
||||||
@ -306,7 +308,7 @@ describe('onWillNavigate', () => {
|
|||||||
const event = { preventDefault };
|
const event = { preventDefault };
|
||||||
await onWillNavigate(options, event, internalURL);
|
await onWillNavigate(options, event, internalURL);
|
||||||
|
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).not.toHaveBeenCalled();
|
expect(preventDefault).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -319,12 +321,12 @@ describe('onWillNavigate', () => {
|
|||||||
const event = { preventDefault };
|
const event = { preventDefault };
|
||||||
await onWillNavigate(options, event, externalURL);
|
await onWillNavigate(options, event, externalURL);
|
||||||
|
|
||||||
expect(mockBlockExternalURL).not.toHaveBeenCalled();
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('external urls should be ignored if blockExternalUrls is true', async () => {
|
test('external urls should be blocked if blockExternalUrls is true', async () => {
|
||||||
const options = {
|
const options = {
|
||||||
blockExternalUrls: true,
|
blockExternalUrls: true,
|
||||||
targetUrl: originalURL,
|
targetUrl: originalURL,
|
||||||
@ -332,7 +334,7 @@ describe('onWillNavigate', () => {
|
|||||||
const event = { preventDefault };
|
const event = { preventDefault };
|
||||||
await onWillNavigate(options, event, externalURL);
|
await onWillNavigate(options, event, externalURL);
|
||||||
|
|
||||||
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -9,12 +9,12 @@ import {
|
|||||||
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
|
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
|
||||||
import * as log from './loggingHelper';
|
import * as log from './loggingHelper';
|
||||||
import {
|
import {
|
||||||
blockExternalURL,
|
|
||||||
createAboutBlankWindow,
|
createAboutBlankWindow,
|
||||||
createNewTab,
|
createNewTab,
|
||||||
injectCSS,
|
injectCSS,
|
||||||
sendParamsOnDidFinishLoad,
|
sendParamsOnDidFinishLoad,
|
||||||
setProxyRules,
|
setProxyRules,
|
||||||
|
showNavigationBlockedMessage,
|
||||||
} from './windowHelpers';
|
} from './windowHelpers';
|
||||||
import { WindowOptions } from '../../../shared/src/options/model';
|
import { WindowOptions } from '../../../shared/src/options/model';
|
||||||
|
|
||||||
@ -86,7 +86,9 @@ export function onNewWindowHelper(
|
|||||||
preventDefault();
|
preventDefault();
|
||||||
if (options.blockExternalUrls) {
|
if (options.blockExternalUrls) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
blockExternalURL(urlToGo)
|
showNavigationBlockedMessage(
|
||||||
|
`Navigation to external URL blocked by options: ${urlToGo}`,
|
||||||
|
)
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
throw err;
|
throw err;
|
||||||
@ -129,7 +131,7 @@ export function onWillNavigate(
|
|||||||
event: Event,
|
event: Event,
|
||||||
urlToGo: string,
|
urlToGo: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.debug('onWillNavigate', { options, event, urlToGo });
|
log.debug('onWillNavigate', urlToGo);
|
||||||
if (
|
if (
|
||||||
!linkIsInternal(
|
!linkIsInternal(
|
||||||
options.targetUrl,
|
options.targetUrl,
|
||||||
@ -141,7 +143,9 @@ export function onWillNavigate(
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (options.blockExternalUrls) {
|
if (options.blockExternalUrls) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
blockExternalURL(urlToGo)
|
showNavigationBlockedMessage(
|
||||||
|
`Navigation to external URL blocked by options: ${urlToGo}`,
|
||||||
|
)
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -23,12 +23,14 @@ export function adjustWindowZoom(adjustment: number): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function blockExternalURL(url: string): Promise<MessageBoxReturnValue> {
|
export function showNavigationBlockedMessage(
|
||||||
|
message: string,
|
||||||
|
): Promise<MessageBoxReturnValue> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
withFocusedWindow((focusedWindow) => {
|
withFocusedWindow((focusedWindow) => {
|
||||||
dialog
|
dialog
|
||||||
.showMessageBox(focusedWindow, {
|
.showMessageBox(focusedWindow, {
|
||||||
message: `Cannot navigate to external URL: ${url}`,
|
message,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Navigation blocked',
|
title: 'Navigation blocked',
|
||||||
})
|
})
|
||||||
|
@ -438,14 +438,14 @@ app.on(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.on('browser-window-blur', (event: Event, window: BrowserWindow) => {
|
app.on('browser-window-blur', () => {
|
||||||
log.debug('app.browser-window-blur', { event, window });
|
log.debug('app.browser-window-blur');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('browser-window-created', (event: Event, window: BrowserWindow) => {
|
app.on('browser-window-created', () => {
|
||||||
log.debug('app.browser-window-created', { event, window });
|
log.debug('app.browser-window-created');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('browser-window-focus', (event: Event, window: BrowserWindow) => {
|
app.on('browser-window-focus', () => {
|
||||||
log.debug('app.browser-window-focus', { event, window });
|
log.debug('app.browser-window-focus');
|
||||||
});
|
});
|
||||||
|
@ -144,10 +144,17 @@ class MockWebRequest {
|
|||||||
|
|
||||||
class InternalEmitter extends EventEmitter {}
|
class InternalEmitter extends EventEmitter {}
|
||||||
|
|
||||||
|
const mockShell = {
|
||||||
|
openExternal(url: string, options?: unknown): Promise<void> {
|
||||||
|
return new Promise((resolve) => resolve());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MockDialog as dialog,
|
MockDialog as dialog,
|
||||||
MockBrowserWindow as BrowserWindow,
|
MockBrowserWindow as BrowserWindow,
|
||||||
MockSession as Session,
|
MockSession as Session,
|
||||||
MockWebContents as WebContents,
|
MockWebContents as WebContents,
|
||||||
MockWebRequest as WebRequest,
|
MockWebRequest as WebRequest,
|
||||||
|
mockShell as shell,
|
||||||
};
|
};
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"test:noplaywright": "jest --testPathIgnorePatterns=playwright",
|
"test:noplaywright": "jest --testPathIgnorePatterns=playwright",
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
"test:watch": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false",
|
"test:watch": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false",
|
||||||
|
"test:watch:unit": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false --testPathIgnorePatterns=integration --testPathIgnorePatterns=playwright",
|
||||||
"test:withlog": "LOGLEVEL=trace npm run test",
|
"test:withlog": "LOGLEVEL=trace npm run test",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"watch": "npx concurrently \"npm:*:watch\""
|
"watch": "npx concurrently \"npm:*:watch\""
|
||||||
|
Loading…
Reference in New Issue
Block a user