diff --git a/main.js b/main.js index cc35d6f..cd0f3e2 100644 --- a/main.js +++ b/main.js @@ -139,13 +139,19 @@ function createWindow(startUrl) { const win = new BrowserWindow(windowOptions); perfMarks.browserWindow_instantiated = performance.now(); - // Allow window.open() popups (e.g. OAuth / SSO / school portals) so that - // POST form submissions and opener relationships are preserved. - // We still restrict to http/https for safety; everything else is denied. - win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('http://') || url.startsWith('https://')) { - return { action: 'allow' }; + // Intercept window.open() requests and route them into the existing window as a new tab + // instead of spawning separate BrowserWindows. We still allow a small OAuth allowlist + // (accounts.google.com, login.microsoftonline.com, oauth, sso) to open real popups if + // the flow depends on window.opener relationships. Everything else becomes a new tab. + win.webContents.setWindowOpenHandler((details) => { + 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' }; }); @@ -161,11 +167,18 @@ function createWindow(startUrl) { // ensure all embedded tags behave predictably without heavy injections win.webContents.on('did-attach-webview', (event, webContents) => { - // Allow popups inside as well (required for some login flows) - webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('http://') || url.startsWith('https://')) { - return { action: 'allow' }; + // Route window.open() calls to tabs unless OAuth allowlist matched + webContents.setWindowOpenHandler((details) => { + const { url } = details; + 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' }; }); }); diff --git a/renderer/script.js b/renderer/script.js index 9023e4a..1a6198b 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -182,9 +182,17 @@ loadBookmarks(); // Remove iframe-based navigation listener (using webview IPC now) // 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); - 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. @@ -290,19 +298,22 @@ function createTab(inputUrl) { webview.addEventListener('did-finish-load', () => { 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 webview.addEventListener('new-window', e => { - // Allow auth / SSO popup windows (don't preventDefault) when target is http(s) - // so form POST + redirect chains stay intact. For simple links attempting to - // open a new tab, we create an in-app tab instead. Heuristic: if disposition - // is 'foreground-tab' or 'background-tab', treat as tab; otherwise allow popup. - if (e.url && (e.url.startsWith('http://') || e.url.startsWith('https://'))) { - if (e.disposition && e.disposition.includes('tab')) { - e.preventDefault(); - createTab(e.url); - } // else let Electron create a real popup window + // Always open external http(s) targets in a new in-app tab instead of spawning + // a separate Electron BrowserWindow. (User request) + // If you need to allow real popup windows for specific OAuth flows later, + // introduce an allowlist check here before preventDefault(). + if (e.url && /^https?:\/\//i.test(e.url)) { + e.preventDefault(); + createTab(e.url); } else { + // Block other scheme popups for safety (could extend with custom handling) e.preventDefault(); } }); @@ -563,12 +574,10 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) { }); webview.addEventListener('new-window', e => { - if (e.url && (e.url.startsWith('http://') || e.url.startsWith('https://'))) { - if (e.disposition && e.disposition.includes('tab')) { - e.preventDefault(); - createTab(e.url); - } - // otherwise allow popup for auth + // Unified behavior: always open http(s) targets in a new tab (no extra window) + if (e.url && /^https?:\/\//i.test(e.url)) { + e.preventDefault(); + createTab(e.url); } else { e.preventDefault(); } @@ -926,12 +935,12 @@ function renderTabs() { } // 1) handle URL sent by main for a detached window -ipcRenderer.on('open-url', (event, url) => { +ipcRenderer.on('open-url', (url) => { tabs = []; activeTabId = null; webviewsEl.innerHTML = ''; tabBarEl.innerHTML = ''; - createTab(url); + if (typeof url === 'string' && url) createTab(url); else createTab(); }); function goBack() {