Route window.open popups to new tabs with OAuth allowlist
Window.open and <webview> popups are now intercepted and routed into new tabs, except for OAuth/SSO flows (Google, Microsoft, etc.) which are allowed to open real popups to preserve authentication semantics. Renderer logic is updated to handle 'open-url-new-tab' IPC events and consistently open external http(s) links in tabs, improving user experience and security.
This commit is contained in:
@@ -139,13 +139,19 @@ function createWindow(startUrl) {
|
|||||||
const win = new BrowserWindow(windowOptions);
|
const win = new BrowserWindow(windowOptions);
|
||||||
perfMarks.browserWindow_instantiated = performance.now();
|
perfMarks.browserWindow_instantiated = performance.now();
|
||||||
|
|
||||||
// Allow window.open() popups (e.g. OAuth / SSO / school portals) so that
|
// Intercept window.open() requests and route them into the existing window as a new tab
|
||||||
// POST form submissions and opener relationships are preserved.
|
// instead of spawning separate BrowserWindows. We still allow a small OAuth allowlist
|
||||||
// We still restrict to http/https for safety; everything else is denied.
|
// (accounts.google.com, login.microsoftonline.com, oauth, sso) to open real popups if
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
// the flow depends on window.opener relationships. Everything else becomes a new tab.
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
win.webContents.setWindowOpenHandler((details) => {
|
||||||
return { action: 'allow' };
|
const { url } = details;
|
||||||
|
if (!/^https?:\/\//i.test(url)) return { action: 'deny' };
|
||||||
|
// OAuth / SSO allowlist heuristic
|
||||||
|
if (/accounts\.google\.com|microsoftonline\.com|oauth|login|signin|sso/i.test(url)) {
|
||||||
|
return { action: 'allow' }; // preserve popup semantics for complex auth flows
|
||||||
}
|
}
|
||||||
|
// Forward to renderer to open as tab
|
||||||
|
try { win.webContents.send('open-url-new-tab', url); } catch {}
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,11 +167,18 @@ function createWindow(startUrl) {
|
|||||||
|
|
||||||
// ensure all embedded <webview> tags behave predictably without heavy injections
|
// ensure all embedded <webview> tags behave predictably without heavy injections
|
||||||
win.webContents.on('did-attach-webview', (event, webContents) => {
|
win.webContents.on('did-attach-webview', (event, webContents) => {
|
||||||
// Allow popups inside <webview> as well (required for some login flows)
|
// Route <webview> window.open() calls to tabs unless OAuth allowlist matched
|
||||||
webContents.setWindowOpenHandler(({ url }) => {
|
webContents.setWindowOpenHandler((details) => {
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
const { url } = details;
|
||||||
return { action: 'allow' };
|
if (!/^https?:\/\//i.test(url)) return { action: 'deny' };
|
||||||
|
if (/accounts\.google\.com|microsoftonline\.com|oauth|login|signin|sso/i.test(url)) {
|
||||||
|
return { action: 'allow' }; // keep popup for auth
|
||||||
}
|
}
|
||||||
|
// Send to the owning window (embedder) to open a new tab
|
||||||
|
try {
|
||||||
|
const host = webContents.hostWebContents || webContents;
|
||||||
|
host.send('open-url-new-tab', url);
|
||||||
|
} catch {}
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+24
-15
@@ -182,9 +182,17 @@ loadBookmarks();
|
|||||||
// Remove iframe-based navigation listener (using webview IPC now)
|
// Remove iframe-based navigation listener (using webview IPC now)
|
||||||
|
|
||||||
// Listen for site history updates from main process
|
// Listen for site history updates from main process
|
||||||
ipcRenderer.on('record-site-history', (event, url) => {
|
// NOTE: electronAPI.on wrapper strips the original event object and only forwards args.
|
||||||
|
// Handlers therefore must NOT expect the event parameter.
|
||||||
|
ipcRenderer.on('record-site-history', (url) => {
|
||||||
debug('[DEBUG] Received site history update:', url);
|
debug('[DEBUG] Received site history update:', url);
|
||||||
addToSiteHistory(url);
|
if (typeof url === 'string' && url) addToSiteHistory(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main process requests opening a URL in a new tab (window.open interception)
|
||||||
|
ipcRenderer.on('open-url-new-tab', (url) => {
|
||||||
|
console.log('[DEBUG] IPC open-url-new-tab received:', url);
|
||||||
|
if (typeof url === 'string' && url) createTab(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-open on download start is disabled by design now.
|
// Auto-open on download start is disabled by design now.
|
||||||
@@ -290,19 +298,22 @@ function createTab(inputUrl) {
|
|||||||
webview.addEventListener('did-finish-load', () => {
|
webview.addEventListener('did-finish-load', () => {
|
||||||
scheduleUpdateNavButtons();
|
scheduleUpdateNavButtons();
|
||||||
});
|
});
|
||||||
|
// Catch failed loads for diagnostics (e.g., http -> https transitions failing)
|
||||||
|
webview.addEventListener('did-fail-load', (e) => {
|
||||||
|
console.warn('[DEBUG] did-fail-load (createTab) id:', id, 'code:', e.errorCode, 'desc:', e.errorDescription, 'validatedURL:', e.validatedURL, 'isMainFrame:', e.isMainFrame);
|
||||||
|
});
|
||||||
|
|
||||||
// catch any target="_blank" or window.open() calls and open them as new tabs
|
// catch any target="_blank" or window.open() calls and open them as new tabs
|
||||||
webview.addEventListener('new-window', e => {
|
webview.addEventListener('new-window', e => {
|
||||||
// Allow auth / SSO popup windows (don't preventDefault) when target is http(s)
|
// Always open external http(s) targets in a new in-app tab instead of spawning
|
||||||
// so form POST + redirect chains stay intact. For simple links attempting to
|
// a separate Electron BrowserWindow. (User request)
|
||||||
// open a new tab, we create an in-app tab instead. Heuristic: if disposition
|
// If you need to allow real popup windows for specific OAuth flows later,
|
||||||
// is 'foreground-tab' or 'background-tab', treat as tab; otherwise allow popup.
|
// introduce an allowlist check here before preventDefault().
|
||||||
if (e.url && (e.url.startsWith('http://') || e.url.startsWith('https://'))) {
|
if (e.url && /^https?:\/\//i.test(e.url)) {
|
||||||
if (e.disposition && e.disposition.includes('tab')) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createTab(e.url);
|
createTab(e.url);
|
||||||
} // else let Electron create a real popup window
|
|
||||||
} else {
|
} else {
|
||||||
|
// Block other scheme popups for safety (could extend with custom handling)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -563,12 +574,10 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
webview.addEventListener('new-window', e => {
|
webview.addEventListener('new-window', e => {
|
||||||
if (e.url && (e.url.startsWith('http://') || e.url.startsWith('https://'))) {
|
// Unified behavior: always open http(s) targets in a new tab (no extra window)
|
||||||
if (e.disposition && e.disposition.includes('tab')) {
|
if (e.url && /^https?:\/\//i.test(e.url)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createTab(e.url);
|
createTab(e.url);
|
||||||
}
|
|
||||||
// otherwise allow popup for auth
|
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -926,12 +935,12 @@ function renderTabs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1) handle URL sent by main for a detached window
|
// 1) handle URL sent by main for a detached window
|
||||||
ipcRenderer.on('open-url', (event, url) => {
|
ipcRenderer.on('open-url', (url) => {
|
||||||
tabs = [];
|
tabs = [];
|
||||||
activeTabId = null;
|
activeTabId = null;
|
||||||
webviewsEl.innerHTML = '';
|
webviewsEl.innerHTML = '';
|
||||||
tabBarEl.innerHTML = '';
|
tabBarEl.innerHTML = '';
|
||||||
createTab(url);
|
if (typeof url === 'string' && url) createTab(url); else createTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
|
|||||||
Reference in New Issue
Block a user