From 1d86cc9c392906f4fc03867baffaae5034196097 Mon Sep 17 00:00:00 2001 From: adamp01 <47503375+adamp01@users.noreply.github.com> Date: Wed, 5 Oct 2022 13:32:19 +0100 Subject: [PATCH] refactor: private tab --- toolkit/modules/PrivateBrowsingUtils.sys.mjs | 5 +- waterfox/browser/components/WaterfoxGlue.jsm | 2 +- .../components/privatetab/PrivateTab.jsm | 503 ------- .../components/privatetab/PrivateTab.sys.mjs | 1250 +++++++++++++++++ .../privatetab/content/privatetab.xhtml | 17 +- .../browser/components/privatetab/moz.build | 2 +- .../test/browser/browser_privatetab.js | 54 +- .../privatetab/test/browser/head.js | 50 +- 8 files changed, 1313 insertions(+), 570 deletions(-) delete mode 100644 waterfox/browser/components/privatetab/PrivateTab.jsm create mode 100644 waterfox/browser/components/privatetab/PrivateTab.sys.mjs diff --git a/toolkit/modules/PrivateBrowsingUtils.sys.mjs b/toolkit/modules/PrivateBrowsingUtils.sys.mjs index 487a43bfbf60..34d2d1094861 100644 --- a/toolkit/modules/PrivateBrowsingUtils.sys.mjs +++ b/toolkit/modules/PrivateBrowsingUtils.sys.mjs @@ -29,7 +29,10 @@ export var PrivateBrowsingUtils = { // This should be used only in frame scripts. isContentWindowPrivate: function pbu_isWindowPrivate(aWindow) { - return this.privacyContextFromWindow(aWindow).usePrivateBrowsing; + // Waterfox: Essential to prevent form data from being saved. + return this.privacyContextFromWindow(aWindow).usePrivateBrowsing + ? true + : Services.prefs.getBoolPref("browser.tabs.selectedTabPrivate", false); }, isBrowserPrivate(aBrowser) { diff --git a/waterfox/browser/components/WaterfoxGlue.jsm b/waterfox/browser/components/WaterfoxGlue.jsm index f6d48c50c304..fee11f62a346 100644 --- a/waterfox/browser/components/WaterfoxGlue.jsm +++ b/waterfox/browser/components/WaterfoxGlue.jsm @@ -20,7 +20,7 @@ XPCOMUtils.defineLazyModuleGetters(lazy, { ChromeManifest: "resource:///modules/ChromeManifest.sys.mjs", Overlays: "resource:///modules/Overlays.sys.mjs", PrefUtils: "resource:///modules/PrefUtils.jsm", - PrivateTab: "resource:///modules/PrivateTab.jsm", + PrivateTab: "resource:///modules/PrivateTab.sys.mjs", StatusBar: "resource:///modules/StatusBar.jsm", TabFeatures: "resource:///modules/TabFeatures.jsm", UICustomizations: "resource:///modules/UICustomizations.jsm", diff --git a/waterfox/browser/components/privatetab/PrivateTab.jsm b/waterfox/browser/components/privatetab/PrivateTab.jsm deleted file mode 100644 index f95d3155f729..000000000000 --- a/waterfox/browser/components/privatetab/PrivateTab.jsm +++ /dev/null @@ -1,503 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const EXPORTED_SYMBOLS = ["PrivateTab"]; - -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -const { ContextualIdentityService } = ChromeUtils.import( - "resource://gre/modules/ContextualIdentityService.jsm" -); - -const { PlacesUIUtils } = ChromeUtils.import( - "resource:///modules/PlacesUIUtils.jsm" -); - -const { TabStateCache } = ChromeUtils.import( - "resource:///modules/sessionstore/TabStateCache.jsm" -); - -const { TabStateFlusher } = ChromeUtils.import( - "resource:///modules/sessionstore/TabStateFlusher.jsm" -); - -const { BrowserUtils } = ChromeUtils.import( - "resource:///modules/BrowserUtils.jsm" -); - -const { PrefUtils } = ChromeUtils.import("resource:///modules/PrefUtils.jsm"); - -const PrivateTab = { - config: { - neverClearData: false, // TODO: change to pref controlled value; if you want to not record history but don"t care about other data, maybe even want to keep private logins - restoreTabsOnRestart: true, - doNotClearDataUntilFxIsClosed: false, - }, - - openTabs: new Set(), - - BTN_ID: "privateTab-button", - BTN2_ID: "newPrivateTab-button", - - get style() { - return ` - @-moz-document url('chrome://browser/content/browser.xhtml') { - #private-mask[enabled="true"] { - display: block !important; - } - #${this.BTN_ID}, #${this.BTN2_ID} { - list-style-image: url(chrome://browser/skin/privateBrowsing.svg); - } - #tabbrowser-tabs[hasadjacentnewprivatetabbutton]:not([overflow="true"]) ~ #${this.BTN_ID}, - #tabbrowser-tabs[overflow="true"] > #tabbrowser-arrowscrollbox > #${this.BTN2_ID}, - #tabbrowser-tabs:not([hasadjacentnewprivatetabbutton]) > #tabbrowser-arrowscrollbox > #${this.BTN2_ID}, - #TabsToolbar[customizing="true"] #${this.BTN2_ID} { - display: none; - } - .tabbrowser-tab[usercontextid="${this.container.userContextId}"] .tab-label { - text-decoration: underline !important; - text-decoration-color: -moz-nativehyperlinktext !important; - text-decoration-style: dashed !important; - } - .tabbrowser-tab[usercontextid="${this.container.userContextId}"][pinned] .tab-icon-image, - .tabbrowser-tab[usercontextid="${this.container.userContextId}"][pinned] .tab-throbber { - border-bottom: 1px dashed -moz-nativehyperlinktext !important; - } - } - `; - }, - - init(window) { - // Only init in a non-private window - if (!window.PrivateBrowsingUtils.isWindowPrivate(window)) { - window.PrivateTab = this; - this.initContainer("Private"); - this.initObservers(window); - this.initListeners(window); - this.initCustomFunctions(window); - this.overridePlacesUIUtils(); - this.updatePrivateMaskId(window); - BrowserUtils.setStyle(this.style); - } - }, - - initContainer(aName) { - ContextualIdentityService.ensureDataReady(); - this.container = ContextualIdentityService._identities.find( - container => container.name == aName - ); - if (this.container && this.container.icon == "private") { - ContextualIdentityService.remove(this.container.userContextId); - this.container = undefined; - } - if (!this.container) { - ContextualIdentityService.create(aName, "fingerprint", "purple"); - this.container = ContextualIdentityService._identities.find( - container => container.name == aName - ); - } else if (!this.config.neverClearData) { - this.clearData(); - } - return this.container; - }, - - clearData() { - Services.clearData.deleteDataFromOriginAttributesPattern({ - userContextId: this.container.userContextId, - }); - }, - - initObservers(aWindow) { - this.setPrivateObserver(); - }, - - initListeners(aWindow) { - this.initPrivateTabListeners(aWindow); - aWindow.document - .getElementById("placesContext") - ?.addEventListener("popupshowing", this.placesContext); - aWindow.document - .getElementById("contentAreaContextMenu") - ?.addEventListener("popupshowing", this.contentContext); - aWindow.document - .getElementById("contentAreaContextMenu") - ?.addEventListener("popuphidden", this.hideContext); - aWindow.document - .getElementById("tabContextMenu") - ?.addEventListener("popupshowing", this.tabContext); - aWindow.document - .getElementById("newPrivateTab-button") - ?.addEventListener("click", this.toolbarClick); - }, - - async updatePrivateMaskId(aWindow) { - let privateMask = aWindow.document.getElementsByClassName( - "private-browsing-indicator" - )[0]; - privateMask.id = "private-mask"; - }, - - setPrivateObserver() { - if (!this.config.neverClearData) { - let observe = () => { - this.clearData(); - if (!this.config.restoreTabsOnRestart) { - this.closeTabs(); - } - }; - Services.obs.addObserver(observe, "quit-application-granted"); - } - }, - - closeTabs() { - ContextualIdentityService._forEachContainerTab((tab, tabbrowser) => { - if (tab.userContextId == this.container.userContextId) { - tabbrowser.removeTab(tab); - } - }); - }, - - placesContext(aEvent) { - let win = aEvent.view; - if (!win) { - return; - } - let { document } = win; - let openAll = "placesContext_openBookmarkContainer:tabs"; - let openAllLinks = "placesContext_openLinks:tabs"; - let openTab = "placesContext_open:newtab"; - // let document = event.target.ownerDocument; - document.getElementById("openPrivate").disabled = document.getElementById( - openTab - ).disabled; - document.getElementById("openPrivate").hidden = document.getElementById( - openTab - ).hidden; - document.getElementById( - "openAllPrivate" - ).disabled = document.getElementById(openAll).disabled; - document.getElementById("openAllPrivate").hidden = document.getElementById( - openAll - ).hidden; - document.getElementById( - "openAllLinksPrivate" - ).disabled = document.getElementById(openAllLinks).disabled; - document.getElementById( - "openAllLinksPrivate" - ).hidden = document.getElementById(openAllLinks).hidden; - }, - - isPrivate(aTab) { - return aTab.getAttribute("usercontextid") == this.container.userContextId; - }, - - contentContext(aEvent) { - let win = aEvent.view; - if (!win) { - return; - } - let { gContextMenu, gBrowser, PrivateTab } = win; - let tab = gBrowser.getTabForBrowser(gContextMenu.browser); - gContextMenu.showItem( - "openLinkInPrivateTab", - gContextMenu.onSaveableLink || gContextMenu.onPlainTextLink - ); - let isPrivate = PrivateTab.isPrivate(tab); - if (isPrivate) { - gContextMenu.showItem("context-openlinkincontainertab", false); - } - }, - - hideContext(aEvent) { - if (!aEvent.view) { - return; - } - if (aEvent.target == this) { - aEvent.view.document.getElementById("openLinkInPrivateTab").hidden = true; - } - }, - - tabContext(aEvent) { - let win = aEvent.view; - if (!win) { - return; - } - let { document, PrivateTab } = win; - const isPrivate = - win.TabContextMenu.contextTab.userContextId === - PrivateTab.container.userContextId; - document - .getElementById("toggleTabPrivateState") - .setAttribute("data-l10n-args", JSON.stringify({ isPrivate })); - }, - - openLink(aEvent) { - let win = aEvent.view; - if (!win) { - return; - } - let { gContextMenu, PrivateTab, document } = win; - win.openLinkIn( - gContextMenu.linkURL, - "tab", - gContextMenu._openLinkInParameters({ - userContextId: PrivateTab.container.userContextId, - triggeringPrincipal: document.nodePrincipal, - }) - ); - }, - - toolbarClick(aEvent) { - let win = aEvent.view; - if (!win) { - return; - } - let { PrivateTab, document } = win; - if (aEvent.button == 0) { - PrivateTab.browserOpenTabPrivate(win); - } else if (aEvent.button == 2) { - document.popupNode = document.getElementById(PrivateTab.BTN_ID); - document - .getElementById("toolbar-context-menu") - .openPopup(this, "after_start", 14, -10, false, false); - document.getElementsByClassName( - "customize-context-removeFromToolbar" - )[0].disabled = false; - document.getElementsByClassName( - "customize-context-moveToPanel" - )[0].disabled = false; - aEvent.preventDefault(); - } - }, - - overridePlacesUIUtils() { - /* globals BrowserWindowTracker */ - // Unused vars required for eval to execute - // eslint-disable-next-line no-unused-vars - const { PlacesUtils } = ChromeUtils.import( - "resource://gre/modules/PlacesUtils.jsm" - ); - // eslint-disable-next-line no-unused-vars - const { PrivateBrowsingUtils } = ChromeUtils.import( - "resource://gre/modules/PrivateBrowsingUtils.jsm" - ); - // eslint-disable-next-line no-unused-vars - function getBrowserWindow(aWindow) { - // Prefer the caller window if it's a browser window, otherwise use - // the top browser window. - return aWindow && - aWindow.document.documentElement.getAttribute("windowtype") == - "navigator:browser" - ? aWindow - : BrowserWindowTracker.getTopWindow(); - } - - // TODO: replace eval with new Function()(); - try { - // eslint-disable-next-line no-eval - eval( - "PlacesUIUtils.openTabset = function " + - PlacesUIUtils.openTabset - .toString() - .replace( - /(\s+)(inBackground: loadInBackground,)/, - "$1$2$1userContextId: aEvent.userContextId || 0," - ) - ); - } catch (ex) {} - }, - - openAllPrivate(event) { - event.userContextId = this.container.userContextId; - PlacesUIUtils.openSelectionInTabs(event); - }, - - openPrivateTab(event) { - let view = event.target.parentElement._view; - PlacesUIUtils._openNodeIn(view.selectedNode, "tab", view.ownerWindow, { - aPrivate: false, - userContextId: this.container.userContextId, - }); - }, - - togglePrivate(aWindow, aTab = aWindow.gBrowser.selectedTab) { - let newTab; - const { gBrowser, gURLBar } = aWindow; - aTab.setAttribute("isToggling", true); - const shouldSelect = aTab == aWindow.gBrowser.selectedTab; - try { - newTab = gBrowser.duplicateTab(aTab); - if (shouldSelect) { - gBrowser.selectedTab = newTab; - const focusUrlbar = gURLBar.focused; - if (focusUrlbar) { - gURLBar.focus(); - } - } - gBrowser.removeTab(aTab); - } catch (ex) { - // Can use this to pop up failure message - } - return newTab; - }, - - browserOpenTabPrivate(aWindow) { - aWindow.openTrustedLinkIn(aWindow.BROWSER_NEW_TAB_URL, "tab", { - userContextId: this.container.userContextId, - }); - }, - - initPrivateTabListeners(aWindow) { - let { gBrowser } = aWindow; - gBrowser.tabContainer.addEventListener("TabSelect", this.onTabSelect); - gBrowser.tabContainer.addEventListener("TabOpen", this.onTabOpen); - - gBrowser.privateListener = e => { - let browser = e.target; - let tab = gBrowser.getTabForBrowser(browser); - if (!tab) { - return; - } - let isPrivate = this.isPrivate(tab); - - if (!isPrivate) { - if (this.observePrivateTabs) { - this.openTabs.delete(tab); - if (!this.openTabs.size) { - this.clearData(); - } - } - return; - } - - if (this.observePrivateTabs) { - this.openTabs.add(tab); - } - - browser.browsingContext.useGlobalHistory = false; - }; - - aWindow.addEventListener("XULFrameLoaderCreated", gBrowser.privateListener); - - if (this.observePrivateTabs) { - gBrowser.tabContainer.addEventListener("TabClose", this.onTabClose); - } - }, - - onTabSelect(aEvent) { - let tab = aEvent.target; - if (!tab) { - return; - } - let win = tab.ownerGlobal; - let { PrivateTab } = win; - let prevTab = aEvent.detail.previousTab; - let isPrivate = PrivateTab.isPrivate(tab); - - if (tab.userContextId != prevTab.userContextId) { - // Show/hide private mask on browser window - PrivateTab.toggleMask(win); - // Ensure we don't save search suggestions for PrivateTab - win.gURLBar.isPrivate = isPrivate; - // Update selected tab private status for autofill - PrefUtils.set("browser.tabs.selectedTabPrivate", isPrivate); - } - }, - - async onTabOpen(aEvent) { - // Update tab state cache - let tab = aEvent.target; - if (!tab) { - return; - } - let { PrivateTab } = tab.ownerGlobal; - let isPrivate = PrivateTab.isPrivate(tab); - // if statement is temp solution to prevent containers being dropped on restart. - // The flushing and cache updating should only occur if the parent tab was - // private and the new tab is non-private OR the parent tab was non-private - // and the new tab is private. Otherwise we should rely on default behaviour. - // We also need to be wary of pinned state of tabs, as that may also have been - // affected in the same case. - if (isPrivate) { - let userContextId = isPrivate ? PrivateTab.container.userContextId : 0; - // Duplicating a tab copies the tab state cache from the parent tab. - // Therefore we need to flush the tab state to ensure it's updated, - // then overwrite the tab usercontextid so that any restored tabs - // are opened in the correct container, rather than that of their - // parent tab. - let browser = tab.linkedBrowser; - // Can't update tab state if we can't get the browser - if (browser) { - TabStateFlusher.flush(browser) - .then(() => { - TabStateCache.update(tab.linkedBrowser.permanentKey, { - isPrivate, - userContextId, - }); - }) - .catch(ex => { - // Sometimes tests fail here - }); - } - } - }, - - onTabClose(aEvent) { - let tab = aEvent.target; - if (!tab) { - return; - } - let { PrivateTab } = tab.ownerGlobal; - if (PrivateTab.isPrivate(tab)) { - PrivateTab.openTabs.delete(tab); - if (!PrivateTab.openTabs.size) { - PrivateTab.clearData(); - } - } - }, - - toggleMask(aWindow) { - let { gBrowser } = aWindow; - let privateMask = aWindow.document.getElementById("private-mask"); - if (gBrowser.selectedTab.isToggling) { - privateMask.setAttribute( - "enabled", - gBrowser.selectedTab.userContextId == this.container.userContextId - ? "false" - : "true" - ); - } else { - privateMask.setAttribute( - "enabled", - gBrowser.selectedTab.userContextId == this.container.userContextId - ? "true" - : "false" - ); - } - }, - - get observePrivateTabs() { - return ( - !this.config.neverClearData && !this.config.doNotClearDataUntilFxIsClosed - ); - }, - - initCustomFunctions(aWindow) { - let { MozElements, PrivateTab } = aWindow; - MozElements.MozTab.prototype.getAttribute = function(att) { - if (att == "usercontextid" && this.getAttribute("isToggling", false)) { - this.removeAttribute("isToggling"); - // If in private tab and we attempt to toggle, remove container, else convert to private tab - return PrivateTab.orig_getAttribute.call(this, att) == - PrivateTab.container.userContextId - ? 0 - : PrivateTab.container.userContextId; - } - return PrivateTab.orig_getAttribute.call(this, att); - }; - }, - - orig_getAttribute: Services.wm.getMostRecentBrowserWindow("navigator:browser") - .MozElements.MozTab.prototype.getAttribute, -}; diff --git a/waterfox/browser/components/privatetab/PrivateTab.sys.mjs b/waterfox/browser/components/privatetab/PrivateTab.sys.mjs new file mode 100644 index 000000000000..89b6c6062546 --- /dev/null +++ b/waterfox/browser/components/privatetab/PrivateTab.sys.mjs @@ -0,0 +1,1250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Lazy load modules for better performance +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserUtils: "resource:///modules/BrowserUtils.sys.mjs", + PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", + ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + ContentSearch: "resource:///actors/ContentSearchParent.sys.mjs", + UrlbarProviderSearchSuggestions: "resource:///modules/UrlbarProviderSearchSuggestions.sys.mjs", + UrlbarProviderRecentSearches: "resource:///modules/UrlbarProviderRecentSearches.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +// Preferences controlling visibility +const PREF_SHOW_NEWTAB_BUTTON = "browser.privateTab.showNewTabButton"; + +// CSS constant for better maintainability and performance +const PRIVATE_TAB_STYLES = { + getCSS(btnId, btn2Id, containerUserContextId) { + return ` + #private-browsing-indicator-with-label[enabled="true"] { + display: inherit !important; + } + #main-window:not([privatebrowsingmode]) #private-browsing-indicator-with-label label { + display: none; + } + .privatetab-icon { + list-style-image: url(chrome://browser/skin/privatebrowsing/favicon.svg) !important; + } + #${btnId}, [id^="${btn2Id}"] { + list-style-image: url(chrome://browser/skin/privateBrowsing.svg) !important; + } + /* Ensure private tab button icon shows properly in vertical mode */ + #vertical-tabs [id^="${btn2Id}"] > .toolbarbutton-icon { + list-style-image: url(chrome://browser/skin/privateBrowsing.svg) !important; + width: 16px !important; + height: 16px !important; + } + /* Container for vertical stacking - match native sidebar alignment */ + #newtab-buttons-container-vertical { + display: flex !important; + flex-direction: column !important; + align-items: start !important; + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; + } + /* Match native vertical new tab button styling for both buttons */ + #newtab-buttons-container-vertical > toolbarbutton { + appearance: none; + min-height: var(--tab-min-height); + line-height: var(--tab-label-line-height); + border-radius: var(--border-radius-medium); + padding: 0 calc(var(--tab-inline-padding) - var(--tab-inner-inline-margin)); + width: var(--tab-collapsed-background-width); + margin-inline: var(--tab-inner-inline-margin); + margin-block: var(--tab-block-margin); + } + /* When expanded, make buttons full width */ + #tabbrowser-tabs[expanded] #newtab-buttons-container-vertical > toolbarbutton { + width: 100%; + } + /* Handle collapsed state - hide text and center buttons */ + #tabbrowser-tabs[orient="vertical"]:not([expanded]) #newtab-buttons-container-vertical { + align-items: center !important; + } + #tabbrowser-tabs[orient="vertical"]:not([expanded]) #newtab-buttons-container-vertical > toolbarbutton > .toolbarbutton-text { + display: none !important; + } + #tabbrowser-tabs[orient="vertical"]:not([expanded]) #newtab-buttons-container-vertical > toolbarbutton { + justify-content: center !important; + } + /* Text alignment and spacing for expanded state */ + #newtab-buttons-container-vertical > toolbarbutton > .toolbarbutton-text { + text-align: left !important; + margin-inline-start: 5px !important; + } + /* Match native sidebar button padding using space variables */ + #newtab-buttons-container-vertical > toolbarbutton { + padding-inline: var(--space-medium) !important; + padding-block: var(--space-xxsmall) !important; + } + /* Hover styles for buttons - combined selector */ + #newtab-buttons-container-vertical > toolbarbutton:is(:hover, :hover:active) { + background-color: var(--toolbarbutton-hover-background) !important; + } + #newtab-buttons-container-vertical > toolbarbutton:hover:active { + background-color: var(--toolbarbutton-active-background) !important; + } + .tabbrowser-tab[usercontextid="${containerUserContextId}"] .tab-label { + text-decoration: underline !important; + text-decoration-color: -moz-nativehyperlinktext !important; + text-decoration-style: dashed !important; + } + .tabbrowser-tab[usercontextid="${containerUserContextId}"][pinned] .tab-icon-image, + .tabbrowser-tab[usercontextid="${containerUserContextId}"][pinned] .tab-throbber { + border-bottom: 1px dashed -moz-nativehyperlinktext !important; + } + `; + } +}; + +export const PrivateTab = { + config: { + longPressDuration: 1000, + doubleclickTime: 450, + doubleclickTimeEnter: 900, + }, + + openTabs: new Set(), + container: null, + + BTN_ID: "privateTab-button", + BTN2_ID: "newPrivateTab-button", + _currentStyleURI: null, + + get style() { + return PRIVATE_TAB_STYLES.getCSS( + this.BTN_ID, + this.BTN2_ID, + this.container?.userContextId + ); + }, + + init(aWindow) { + // Only init in non-private windows + if (lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + return; + } + + // Wait for XUL elements to be available + if (!aWindow.document.getElementById("toggleTabPrivateState")) { + lazy.setTimeout(() => this.init(aWindow), 50); + return; + } + + aWindow.PrivateTab = this; + this.initContainer("Private"); + this.initObservers(aWindow); + this.createToolbarButton(aWindow); + this.observeTabsOrientation(aWindow); + this.initListeners(aWindow); + this.initPrivateTabListeners(aWindow); + this.initCustomFunctions(aWindow); + this.overridePlacesUIUtils(); + this.overrideContentSearchParent(); + this.overrideUrlbarProviders(); + this.overrideUrlbarUtils(); + this.overrideSessionStore(aWindow); + + // Clean up observers on window unload + aWindow.addEventListener("unload", () => { + this.cleanupObservers(aWindow); + }, { once: true }); + + // Update private browsing indicator + const privateIndicator = aWindow.document.getElementById("private-browsing-indicator-with-label"); + if (privateIndicator && aWindow.gBrowser.selectedTab?.userContextId === this.container.userContextId) { + privateIndicator.setAttribute("enabled", "true"); + } + + this.applyStyle(); + this.updateUIVisibility(aWindow); + }, + + initContainer(aName) { + try { + lazy.ContextualIdentityService.ensureDataReady(); + this.container = lazy.ContextualIdentityService._identities.find( + (container) => container.name === aName + ); + if (!this.container) { + try { + lazy.ContextualIdentityService.create(aName, "fingerprint", "purple"); + } catch (createEx) { + if (createEx.message?.includes("Component is not available")) { + console.error("PrivateTab initContainer create error:", createEx.message); + console.error("Stack:", new Error().stack); + } + throw createEx; + } + this.container = lazy.ContextualIdentityService._identities.find( + (container) => container.name === aName + ); + } else if (!this.config.neverClearData) { + this.clearData(); + } + } catch (ex) { + if (ex.message?.includes("Component is not available")) { + console.error("PrivateTab initContainer error:", ex.message); + console.error("Stack:", new Error().stack); + } + } + return this.container; + }, + + clearData() { + if (!this.container?.userContextId) { + return; + } + + try { + if (Services && Services.clearData && Services.clearData.deleteDataFromOriginAttributesPattern) { + try { + Services.clearData.deleteDataFromOriginAttributesPattern({ + userContextId: this.container.userContextId, + }); + } catch (innerEx) { + console.error("PrivateTab clearData error:", innerEx.message); + } + } + } catch (ex) { + console.error("PrivateTab clearData outer error:", ex.message); + console.error(" Full error:", ex); + console.error(" Stack:", new Error().stack); + } + }, + + // Robust startup cleanup for crash scenarios and improper shutdowns + cleanupStartupTabs(aWindow) { + const { gBrowser } = aWindow; + if (!gBrowser) return; + + // Check and clean up private tabs + const doCleanup = () => { + try { + // Don't run if browser is still initializing + if (!gBrowser.tabs || gBrowser.tabs.length === 0) return; + + const privateTabs = []; + for (let tab of gBrowser.tabs) { + if (this.isPrivate(tab)) { + privateTabs.push(tab); + } + } + + if (privateTabs.length === 0) return; + + // If ALL tabs are private, create a regular tab first + if (privateTabs.length === gBrowser.tabs.length) { + try { + const principal = Services.scriptSecurityManager.getSystemPrincipal(); + const newTab = gBrowser.addTab("about:home", { + userContextId: 0, + triggeringPrincipal: principal + }); + gBrowser.selectedTab = newTab; + } catch (ex) { + // Fallback without principal if Services aren't ready + const newTab = gBrowser.addTab("about:home"); + gBrowser.selectedTab = newTab; + } + } + + // Remove all private tabs + for (let tab of privateTabs) { + if (gBrowser.tabs.length > 1) { + try { + gBrowser.removeTab(tab); + } catch (ex) { + // Tab might already be closing + } + } + } + + if (privateTabs.length > 0) { + this.clearData(); + } + } catch (ex) { + // Cleanup failed, will retry + } + }; + + // Wait longer for session restore to be ready + aWindow.setTimeout(() => { + doCleanup(); + // Run again after session restore completes + aWindow.setTimeout(doCleanup, 2000); + }, 1000); + }, + + initObservers(aWindow) { + this.setPrivateObserver(aWindow); + // Clean up startup tabs after session restore + this.cleanupStartupTabs(aWindow); + // Observe preference changes that control visibility + this.initPrefObservers(aWindow); + }, + + cleanupPrivateTabButtons(aWindow) { + const doc = aWindow.document; + // Remove all existing private tab buttons + doc.querySelectorAll(`[id^="${this.BTN2_ID}"]`).forEach(btn => btn.remove()); + + // Clean up vertical container if it exists + const verticalContainer = doc.getElementById("newtab-buttons-container-vertical"); + if (verticalContainer) { + // Move the new tab button back to its original position + const newTabButton = verticalContainer.querySelector("#tabs-newtab-button"); + if (newTabButton && verticalContainer.parentNode) { + verticalContainer.parentNode.insertBefore(newTabButton, verticalContainer); + } + verticalContainer.remove(); + } + }, + + createToolbarButton(aWindow) { + const doc = aWindow.document; + + // Clean up any existing buttons first + this.cleanupPrivateTabButtons(aWindow); + + // Respect preference controlling visibility of the new private tab button + const showNewTabButton = Services.prefs.getBoolPref(PREF_SHOW_NEWTAB_BUTTON, true); + if (!showNewTabButton) { + return; + } + + // Find all instances of new tab buttons + doc.querySelectorAll("#tabs-newtab-button").forEach((tabsNewTabButton, index) => { + // Check if this button is in vertical tabs mode + const closestTabs = tabsNewTabButton.closest("tabs"); + const isVerticalMode = tabsNewTabButton.closest("#vertical-tabs") !== null || + (closestTabs?.getAttribute("orient") === "vertical"); + + // Create unique ID for each location + const buttonId = isVerticalMode ? `${this.BTN2_ID}-vertical-${index}` : this.BTN2_ID; + + // Only create if it doesn't already exist + if (!doc.getElementById(buttonId)) { + // Create the private tab button + const btn2 = doc.createXULElement("toolbarbutton"); + btn2.id = buttonId; + // Use same class as the new tab button for consistent styling in vertical mode + if (isVerticalMode) { + btn2.className = tabsNewTabButton.className || "toolbarbutton-1"; + } else { + // For horizontal mode, use standard toolbar button styling + btn2.className = "toolbarbutton-1 chromeclass-toolbar-additional"; + } + btn2.setAttribute("label", "New Private Tab"); + btn2.setAttribute("tooltiptext", "Open a new private tab (Ctrl+Alt+P)"); + + if (isVerticalMode) { + // Create the button structure to match native sidebar buttons + const icon = doc.createXULElement("image"); + icon.className = "toolbarbutton-icon"; + icon.setAttribute("label", "New Private Tab"); + + const text = doc.createXULElement("label"); + text.className = "toolbarbutton-text"; + text.setAttribute("crop", "end"); + text.setAttribute("flex", "1"); + text.setAttribute("value", "New Private Tab"); + + // Clear any existing content and add new elements + while (btn2.firstChild) { + btn2.removeChild(btn2.firstChild); + } + btn2.appendChild(icon); + btn2.appendChild(text); + + // Find or create the vertical container + let container = doc.getElementById("newtab-buttons-container-vertical"); + if (!container) { + container = doc.createXULElement("vbox"); + container.id = "newtab-buttons-container-vertical"; + + // Find the periphery container + const periphery = tabsNewTabButton.parentNode; + if (periphery && periphery.id === "tabbrowser-arrowscrollbox-periphery") { + // Find the spacer element + const spacer = periphery.querySelector(".closing-tabs-spacer"); + + // Insert container before spacer or at end + if (spacer) { + periphery.insertBefore(container, spacer); + } else { + periphery.appendChild(container); + } + + // Move the new tab button into the container + container.appendChild(tabsNewTabButton); + } + } + + // Add the private tab button to the container + if (container) { + container.appendChild(btn2); + } else { + // Fallback + tabsNewTabButton.insertAdjacentElement("afterend", btn2); + } + } else { + // For horizontal tabs toolbar, keep side-by-side placement + tabsNewTabButton.insertAdjacentElement("afterend", btn2); + } + } + }); + }, + + observeTabsOrientation(aWindow) { + const doc = aWindow.document; + + // Unified handler for orientation/visibility changes + const handleOrientationChange = () => { + this.updateUIVisibility(aWindow); + }; + + // Watch for changes to tabs orientation + const tabsElement = doc.querySelector("tabs#tabbrowser-tabs"); + if (tabsElement) { + this.orientationObserver = new aWindow.MutationObserver(mutations => { + if (mutations.some(m => m.attributeName === 'orient')) { + handleOrientationChange(); + } + }); + this.orientationObserver.observe(tabsElement, { + attributes: true, + attributeFilter: ['orient'] + }); + } + + // Watch for changes to vertical-tabs visibility + const verticalTabsBox = doc.querySelector("#vertical-tabs"); + if (verticalTabsBox) { + this.verticalTabsObserver = new aWindow.MutationObserver(mutations => { + if (mutations.some(m => m.attributeName === 'collapsed' || m.attributeName === 'hidden')) { + handleOrientationChange(); + } + }); + this.verticalTabsObserver.observe(verticalTabsBox, { + attributes: true, + attributeFilter: ['collapsed', 'hidden'] + }); + } + }, + + cleanupObservers(aWindow) { + // Disconnect all observers + ['orientationObserver', 'verticalTabsObserver'].forEach(observerName => { + if (this[observerName]) { + this[observerName].disconnect(); + this[observerName] = null; + } + }); + + // Remove pref observers + if (this.prefObserver) { + try { + Services.prefs.removeObserver(PREF_SHOW_NEWTAB_BUTTON, this.prefObserver); + } catch (ex) { + // Silently ignore + } + this.prefObserver = null; + } + }, + + applyStyle() { + try { + const css = this.style; + const uri = `data:text/css;charset=UTF-8,${encodeURIComponent(css)}`; + if (this._currentStyleURI && this._currentStyleURI !== uri) { + lazy.BrowserUtils.unregisterStylesheet(this._currentStyleURI); + } + // Register the new stylesheet and remember it + lazy.BrowserUtils.registerStylesheet(uri); + this._currentStyleURI = uri; + } catch (e) { + // No-op + } + }, + + updateUIVisibility(aWindow) { + try { + const doc = aWindow.document; + + const showNewTabButton = Services.prefs.getBoolPref(PREF_SHOW_NEWTAB_BUTTON, true); + + // Rebuild New Private Tab buttons according to pref + if (showNewTabButton) { + this.createToolbarButton(aWindow); + this.initPrivateTabButtonListeners(aWindow); + } else { + this.cleanupPrivateTabButtons(aWindow); + } + } catch (e) { + // No-op + } + }, + + initPrefObservers(aWindow) { + // Observe preferences for visibility changes of toolbar and new tab buttons + if (!this.prefObserver) { + this.prefObserver = { + observe: (subject, topic, data) => { + if (topic !== "nsPref:changed") { + return; + } + if (data === PREF_SHOW_NEWTAB_BUTTON) { + try { + // Re-apply styles and update UI based on new pref values + this.applyStyle(); + this.updateUIVisibility(aWindow); + } catch (e) { + // No-op + } + } + }, + }; + } + + try { + Services.prefs.addObserver(PREF_SHOW_NEWTAB_BUTTON, this.prefObserver); + } catch (ex) { + // Pref service may not be available in some early/late lifecycle stages + } + }, + + initPrivateTabButtonListeners(aWindow) { + const doc = aWindow.document; + + // Handler for private tab button clicks + const handleClick = (e) => { + if (e.button === 0) { + this.browserOpenTabPrivate(aWindow); + } else if (e.button === 2) { + doc.getElementById("toolbar-context-menu")?.openPopup( + e.currentTarget, "after_start", 14, -10, false, false + ); + e.preventDefault(); + } + }; + + // Attach listeners to all private tab buttons + doc.querySelectorAll(`[id^="${this.BTN2_ID}"]`).forEach(btn2 => { + // Remove any existing listeners by cloning + const newBtn2 = btn2.cloneNode(true); + btn2.parentNode?.replaceChild(newBtn2, btn2); + newBtn2.addEventListener("click", handleClick); + }); + }, + + initListeners(aWindow) { + const doc = aWindow.document; + + // Keyboard shortcuts + doc.getElementById("togglePrivateTab-key")?.addEventListener("command", () => { + this.togglePrivate(aWindow); + }); + + doc.getElementById("newPrivateTab-key")?.addEventListener("command", () => { + this.browserOpenTabPrivate(aWindow); + }); + + // Menu items + doc.getElementById("menu_newPrivateTab")?.addEventListener("command", () => { + this.browserOpenTabPrivate(aWindow); + }); + + // Toggle tab private state menu item + doc.getElementById("toggleTabPrivateState")?.addEventListener("command", () => { + if (aWindow.TabContextMenu?.contextTab) { + this.togglePrivate(aWindow, aWindow.TabContextMenu.contextTab); + } else { + this.togglePrivate(aWindow); + } + }); + + // Context menu - open link in private tab + doc.getElementById("openLinkInPrivateTab")?.addEventListener("command", () => { + this.openLink(aWindow); + }); + + // Places context menu items + doc.getElementById("openPrivate")?.addEventListener("command", (event) => { + this.openPrivateTab(event); + }); + + doc.getElementById("openAllPrivate")?.addEventListener("command", (event) => { + this.openAllPrivate(event); + }); + + doc.getElementById("openAllLinksPrivate")?.addEventListener("command", (event) => { + this.openAllPrivate(event); + }); + + // Context menu popup listeners + doc.getElementById("contentAreaContextMenu")?.addEventListener( + "popupshowing", + this.contentContext.bind(this) + ); + + doc.getElementById("contentAreaContextMenu")?.addEventListener( + "popuphidden", + this.hideContext.bind(this) + ); + + doc.getElementById("tabContextMenu")?.addEventListener( + "popupshowing", + this.tabContext.bind(this) + ); + + doc.getElementById("placesContext")?.addEventListener( + "popupshowing", + this.placesContext.bind(this) + ); + + // Initialize private tab button listeners + this.initPrivateTabButtonListeners(aWindow); + }, + + setPrivateObserver(aWindow) { + // Handle browser shutdown + const shutdownObserver = () => { + try { + // Close all private tabs before shutdown + this.closeAllPrivateTabs(); + // Clear data after closing tabs + this.clearData(); + } catch (ex) { + // Silently fail during shutdown + } + }; + + // Use multiple shutdown events to ensure cleanup happens + try { + Services.obs.addObserver(shutdownObserver, "quit-application-requested"); + Services.obs.addObserver(shutdownObserver, "quit-application"); + Services.obs.addObserver(shutdownObserver, "sessionstore-windows-restored"); + } catch (ex) { + // Silently fail if observer service is unavailable + } + + // Also handle window close directly with beforeunload for earlier intervention + const cleanupHandler = () => { + try { + // If this is the last window, clean up + if (Services && Services.wm) { + const windows = Services.wm.getEnumerator("navigator:browser"); + let windowCount = 0; + while (windows.hasMoreElements()) { + windows.getNext(); + windowCount++; + } + + if (windowCount <= 1) { + try { + this.closeAllPrivateTabs(); + this.clearData(); + } catch (ex) { + // Silently fail + } + } + } + } catch (ex) { + // Silently fail + } + }; + + aWindow.addEventListener("beforeunload", cleanupHandler); + aWindow.addEventListener("unload", cleanupHandler); + }, + + closeTabs() { + if (!this.container?.userContextId) return; + try { + lazy.ContextualIdentityService._forEachContainerTab((tab, tabbrowser) => { + if (tab.userContextId == this.container.userContextId) { + tabbrowser.removeTab(tab); + } + }); + } catch (ex) { + // Service might not be available + } + }, + + closeAllPrivateTabs() { + // Close private tabs in all windows before shutdown + try { + if (!Services || !Services.wm) return; + + const windows = Services.wm.getEnumerator("navigator:browser"); + const windowList = []; + while (windows.hasMoreElements()) { + windowList.push(windows.getNext()); + } + + for (let win of windowList) { + if (!win || !win.gBrowser) continue; + + const tabsToClose = []; + for (let tab of win.gBrowser.tabs) { + if (this.isPrivate(tab)) { + tabsToClose.push(tab); + } + } + + if (tabsToClose.length === 0) continue; + + // If ALL tabs are private, create a regular tab first + if (tabsToClose.length === win.gBrowser.tabs.length) { + try { + const principal = Services.scriptSecurityManager?.getSystemPrincipal(); + if (principal) { + win.gBrowser.addTab("about:blank", { + userContextId: 0, + triggeringPrincipal: principal + }); + } else { + win.gBrowser.addTab("about:blank"); + } + } catch (ex) { + // Window might be closing, try without options + try { + win.gBrowser.addTab("about:blank"); + } catch (ex2) { + // Give up + } + } + } + + // Now close the private tabs + for (let tab of tabsToClose) { + if (win.gBrowser && win.gBrowser.tabs.length > 1) { + try { + win.gBrowser.removeTab(tab); + } catch (ex) { + // Tab might already be closing + } + } + } + } + } catch (ex) { + // Window manager might not be available during shutdown + } + }, + + placesContext(aEvent) { + const win = aEvent.view || aEvent.target.ownerGlobal; + const doc = win.document; + const openPrivate = doc.getElementById("openPrivate"); + const openAllPrivate = doc.getElementById("openAllPrivate"); + const openAllLinksPrivate = doc.getElementById("openAllLinksPrivate"); + const openTab = doc.getElementById("placesContext_open:newtab"); + const openAll = doc.getElementById("placesContext_openBookmarkContainer:tabs"); + const openAllLinks = doc.getElementById("placesContext_openLinks:tabs"); + + if (openPrivate && openTab) { + openPrivate.disabled = openTab.disabled; + openPrivate.hidden = openTab.hidden; + } + if (openAllPrivate && openAll) { + openAllPrivate.disabled = openAll.disabled; + openAllPrivate.hidden = openAll.hidden; + } + if (openAllLinksPrivate && openAllLinks) { + openAllLinksPrivate.disabled = openAllLinks.disabled; + openAllLinksPrivate.hidden = openAllLinks.hidden; + } + }, + + isPrivate(aTab) { + // Ensure we have a valid container before checking + if (!this.container?.userContextId) return false; + // Use == not === to handle string/number comparison + return aTab.getAttribute("usercontextid") == this.container.userContextId; + }, + + contentContext(aEvent) { + const win = aEvent.view || aEvent.target?.ownerGlobal; + if (!win) { + return; + } + const gContextMenu = win.gContextMenu; + + // Don't show private tab options in the sidebar + if (gContextMenu.browser == win.SidebarController.treeVerticalTabsBrowser) { + return; + } + + const tab = win.gBrowser.getTabForBrowser(gContextMenu.browser); + const openLinkInPrivateTab = win.document.getElementById("openLinkInPrivateTab"); + + if (openLinkInPrivateTab) { + gContextMenu.showItem( + "openLinkInPrivateTab", + gContextMenu.onSaveableLink || gContextMenu.onPlainTextLink + ); + } + + const isPrivate = this.isPrivate(tab); + if (isPrivate) { + gContextMenu.showItem("context-openlinkincontainertab", false); + } + }, + + hideContext(aEvent) { + if (aEvent.target === aEvent.currentTarget) { + const win = aEvent.view || aEvent.target?.ownerGlobal; + const openLink = win?.document.getElementById("openLinkInPrivateTab"); + if (openLink) { + openLink.hidden = true; + } + } + }, + + tabContext(aEvent) { + const win = aEvent.view || aEvent.target?.ownerGlobal; + if (!win) { + return; + } + const toggleTab = win.document.getElementById("toggleTabPrivateState"); + if (toggleTab && win.TabContextMenu?.contextTab) { + toggleTab.setAttribute( + "checked", + win.TabContextMenu.contextTab.userContextId == this.container?.userContextId + ); + } + }, + + openLink(aWindow) { + if (!this.container?.userContextId) return; + const { gContextMenu } = aWindow; + aWindow.openLinkIn( + gContextMenu.linkURL, + "tab", + gContextMenu._openLinkInParameters({ + userContextId: this.container.userContextId, + triggeringPrincipal: aWindow.document.nodePrincipal, + }) + ); + }, + + overridePlacesUIUtils() { + const originalOpenTabset = lazy.PlacesUIUtils.openTabset; + lazy.PlacesUIUtils.openTabset = function ( + aEvent, + aWindow, + aTabs, + loadInBackground + ) { + return originalOpenTabset.call( + this, + aEvent, + aWindow, + aTabs, + loadInBackground, + aEvent.userContextId || 0 + ); + }; + }, + + // Prevent saving search form history for Private container tabs (urlbar/searchbar) + overrideContentSearchParent() { + if (this._contentSearchPatched) { + return; + } + this._contentSearchPatched = true; + try { + const originalAddFormHistoryEntry = + lazy.ContentSearch.addFormHistoryEntry.bind(lazy.ContentSearch); + lazy.ContentSearch.addFormHistoryEntry = async (browser, entry = null) => { + try { + const win = browser?.ownerGlobal; + const tab = + win?.gBrowser?.getTabForBrowser && + win.gBrowser.getTabForBrowser(browser); + if (tab && this.isPrivate(tab)) { + // Do not store form history for searches from Private container tabs + return false; + } + } catch (e) { + // Ignore and fall through to original + } + return originalAddFormHistoryEntry(browser, entry); + }; + } catch (e) { + // Silently ignore if actor is unavailable + } + }, + + // Helper to detect Private container queries in urlbar contexts + _isPrivateQueryContext(queryContext) { + return !!this.container?.userContextId && + queryContext?.userContextId == this.container.userContextId; + }, + + // Suppress urlbar suggestions providers that leak search data in Private container tabs + overrideUrlbarProviders() { + if (this._urlbarProvidersPatched) { + return; + } + this._urlbarProvidersPatched = true; + + // Disable remote and local search suggestions provider for Private container + try { + const prov = lazy.UrlbarProviderSearchSuggestions; + if (prov && typeof prov.isActive == "function") { + const origIsActive = prov.isActive.bind(prov); + prov.isActive = async queryContext => { + if (this._isPrivateQueryContext(queryContext)) { + return false; + } + return origIsActive(queryContext); + }; + } + } catch (e) { + // No-op + } + + // Disable recent searches provider for Private container + try { + const recent = lazy.UrlbarProviderRecentSearches; + if (recent && typeof recent.isActive == "function") { + const origRecentActive = recent.isActive.bind(recent); + recent.isActive = async queryContext => { + if (this._isPrivateQueryContext(queryContext)) { + return false; + } + return origRecentActive(queryContext); + }; + } + } catch (e) { + // No-op + } + }, + + // Prevent urlbar form/input history writes from Private container tabs + overrideUrlbarUtils() { + if (this._urlbarUtilsPatched) { + return; + } + this._urlbarUtilsPatched = true; + + // Block saving form history (search terms) when Private container is active + try { + const origAddToFormHistory = + lazy.UrlbarUtils.addToFormHistory.bind(lazy.UrlbarUtils); + lazy.UrlbarUtils.addToFormHistory = (input, value, source) => { + try { + const win = input?.window || input?.ownerGlobal; + const uci = parseInt( + win?.gBrowser?.selectedBrowser?.getAttribute("usercontextid") || 0 + ); + if (uci == this.container?.userContextId) { + return Promise.resolve(); + } + } catch (e) { + // No-op + } + return origAddToFormHistory(input, value, source); + }; + } catch (e) { + // No-op + } + + // Block saving adaptive input history when Private container is active + try { + const origAddToInputHistory = + lazy.UrlbarUtils.addToInputHistory.bind(lazy.UrlbarUtils); + lazy.UrlbarUtils.addToInputHistory = async (url, input) => { + try { + const win = lazy.BrowserUtils.mostRecentWindow; + const uci = parseInt( + win?.gBrowser?.selectedBrowser?.getAttribute("usercontextid") || 0 + ); + if (uci == this.container?.userContextId) { + return; + } + } catch (e) { + // No-op + } + return origAddToInputHistory(url, input); + }; + } catch (e) { + // No-op + } + }, + + openAllPrivate(event) { + if (!this.container?.userContextId) return; + event.userContextId = this.container.userContextId; + lazy.PlacesUIUtils.openSelectionInTabs(event); + }, + + openPrivateTab(event) { + if (!this.container?.userContextId) return; + const view = event.target.parentElement._view; + if (view && view.selectedNode) { + lazy.PlacesUIUtils._openNodeIn(view.selectedNode, "tab", view.ownerWindow, { + aPrivate: false, + userContextId: this.container.userContextId, + }); + } + }, + + togglePrivate(aWindow, aTab = aWindow.gBrowser.selectedTab) { + const { gBrowser, gURLBar } = aWindow; + + // Check if container is properly initialized + if (!this.container?.userContextId) { + console.error("PrivateTab: Container not initialized for toggle"); + return null; + } + + aTab.isToggling = true; + const shouldSelect = aTab === gBrowser.selectedTab; + + const newTab = gBrowser.duplicateTab(aTab); + const newBrowser = newTab.linkedBrowser; + + // Update tab state cache after duplication with the new container ID + aWindow.addEventListener("SSWindowStateReady", () => { + try { + const newContextId = parseInt(newTab.getAttribute("usercontextid")) || 0; + lazy.TabStateCache.update(newBrowser.permanentKey, { + userContextId: newContextId + }); + } catch (ex) { + if (ex.message?.includes("Component is not available")) { + console.error("PrivateTab TabStateCache.update error:", ex.message); + console.error("Stack:", new Error().stack); + } + } + }, { once: true }); + + if (shouldSelect) { + const focusUrlbar = gURLBar.focused; + gBrowser.selectedTab = newTab; + if (focusUrlbar) { + gURLBar.focus(); + } + } + + gBrowser.removeTab(aTab); + return newTab; + }, + + browserOpenTabPrivate(aWindow) { + if (!this.container?.userContextId) { + console.warn("PrivateTab: Container not initialized"); + return; + } + + try { + aWindow.openTrustedLinkIn(aWindow.BROWSER_NEW_TAB_URL, "tab", { + userContextId: this.container.userContextId, + }); + } catch (ex) { + console.error("PrivateTab browserOpenTabPrivate error:", ex.message); + console.error("Full error:", ex); + console.error("Stack:", new Error().stack); + throw ex; + } + }, + + initPrivateTabListeners(aWindow) { + const { gBrowser } = aWindow; + + gBrowser.tabContainer.addEventListener( + "TabSelect", + this.onTabSelect.bind(this) + ); + + // Add initial check for selected tab + if (gBrowser.selectedTab && this.isPrivate(gBrowser.selectedTab)) { + this.toggleMask(aWindow); + } + + gBrowser.privateListener = (e) => { + try { + const browser = e.target; + if (!browser) return; + + const tab = gBrowser.getTabForBrowser(browser); + if (!tab) return; + + const isPrivate = this.isPrivate(tab); + + // Exit early for non-private tabs - no need to process or log + if (!isPrivate) { + // Only handle cleanup if we're observing private tabs + if (this.observePrivateTabs && this.openTabs.has(tab)) { + this.openTabs.delete(tab); + if (!this.openTabs.size) { + this.clearData(); + } + } + return; + } + + if (this.observePrivateTabs) { + this.openTabs.add(tab); + } + + // Prevent history storage for private tabs + // NOTE: This will generate NS_ERROR_NOT_AVAILABLE errors in the console. + // These errors are harmless and expected - they occur because Firefox's + // internal components try to access the history service after we disable it. + // The errors don't affect functionality and private tabs still work correctly. + try { + if (browser.browsingContext && !browser.browsingContext.closed) { + browser.browsingContext.useGlobalHistory = false; + } + } catch (ex) { + // Silently ignore errors - the property might not be available yet + } + } catch (ex) { + console.error("PrivateTab privateListener error:", ex.message); + console.error(" Full error:", ex); + console.error(" Event type:", e?.type); + console.error(" Stack:", new Error().stack); + } + }; + + aWindow.addEventListener("XULFrameLoaderCreated", gBrowser.privateListener); + + if (this.observePrivateTabs) { + gBrowser.tabContainer.addEventListener( + "TabClose", + this.onTabClose.bind(this) + ); + } + }, + + onTabSelect(aEvent) { + const tab = aEvent.target; + const win = tab.ownerGlobal; + const prevTab = aEvent.detail.previousTab; + + if (tab.userContextId != prevTab.userContextId) { + this.toggleMask(win); + } + // Clear any existing urlbar view data when switching into a Private container tab + if (this.isPrivate(tab)) { + try { + win.gURLBar?.view?.clear?.(); + } catch (e) { + // No-op + } + } + }, + + onTabClose(aEvent) { + try { + const tab = aEvent.target; + if (this.isPrivate(tab)) { + this.openTabs.delete(tab); + if (!this.openTabs.size) { + // Silently try to clear data + try { + this.clearData(); + } catch (ex) { + // Silently fail + } + } + } + } catch (ex) { + // Silently fail if any component is unavailable + } + }, + + toggleMask(aWindow) { + const { gBrowser } = aWindow; + const privateIndicator = aWindow.document.getElementById( + "private-browsing-indicator-with-label" + ); + if (!privateIndicator) return; + + if (gBrowser.selectedTab.isToggling) { + privateIndicator.setAttribute( + "enabled", + gBrowser.selectedTab.userContextId == this.container?.userContextId ? "false" : "true" + ); + } else { + privateIndicator.setAttribute( + "enabled", + gBrowser.selectedTab.userContextId == this.container?.userContextId ? "true" : "false" + ); + } + }, + + get observePrivateTabs() { + return !this.config.neverClearData && !this.config.doNotClearDataUntilFxIsClosed; + }, + + initCustomFunctions(aWindow) { + const { MozElements } = aWindow; + + // Store original getAttribute + if (!this.orig_getAttribute) { + this.orig_getAttribute = MozElements.MozTab.prototype.getAttribute; + } + + // Override getAttribute to handle toggling + MozElements.MozTab.prototype.getAttribute = function (att) { + if (att == "usercontextid" && this.isToggling) { + delete this.isToggling; + const currentId = PrivateTab.orig_getAttribute.call(this, att); + // If current tab is private, return 0 (regular), otherwise return private container ID + return currentId == PrivateTab.container?.userContextId ? "0" : String(PrivateTab.container?.userContextId || 0); + } else { + return PrivateTab.orig_getAttribute.call(this, att); + } + }; + }, + + // Session store override to prevent private tab persistence + overrideSessionStore(aWindow) { + const { gBrowser } = aWindow; + if (!gBrowser) return; + + // Mark private tabs as not restorable when they're created + gBrowser.addEventListener("TabOpen", (e) => { + const tab = e.target; + if (this.isPrivate(tab)) { + // Delete from cache to prevent session storage + try { + if (lazy.TabStateCache && tab.linkedBrowser?.permanentKey) { + lazy.TabStateCache.delete(tab.linkedBrowser.permanentKey); + } + } catch (ex) { + // TabStateCache might not be available - silently continue + } + } + }); + + // Clear private tab state periodically + gBrowser.addEventListener("TabSelect", (e) => { + const tab = e.target; + if (this.isPrivate(tab)) { + try { + if (lazy.TabStateCache && tab.linkedBrowser?.permanentKey) { + lazy.TabStateCache.delete(tab.linkedBrowser.permanentKey); + } + } catch (ex) { + // TabStateCache might not be available - silently continue + } + } + }); + }, +}; diff --git a/waterfox/browser/components/privatetab/content/privatetab.xhtml b/waterfox/browser/components/privatetab/content/privatetab.xhtml index 72624c9db710..3c37eda19cd4 100644 --- a/waterfox/browser/components/privatetab/content/privatetab.xhtml +++ b/waterfox/browser/components/privatetab/content/privatetab.xhtml @@ -8,35 +8,34 @@ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - - + + + insertafter="context_pinTab" /> + insertafter="placesContext_openBookmarkLinks:tabs" /> + insertafter="placesContext_open:newtab" /> + insertafter="placesContext_openLinks:tabs" /> + insertafter="menu_newNavigatorTab" /> + insertafter="context-openlinkintab" /> diff --git a/waterfox/browser/components/privatetab/moz.build b/waterfox/browser/components/privatetab/moz.build index 02e5a84d88e0..05073c049e17 100644 --- a/waterfox/browser/components/privatetab/moz.build +++ b/waterfox/browser/components/privatetab/moz.build @@ -5,7 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXTRA_JS_MODULES += [ - "PrivateTab.jsm", + "PrivateTab.sys.mjs", ] BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] diff --git a/waterfox/browser/components/privatetab/test/browser/browser_privatetab.js b/waterfox/browser/components/privatetab/test/browser/browser_privatetab.js index 60dab995780d..53f4ecc88312 100644 --- a/waterfox/browser/components/privatetab/test/browser/browser_privatetab.js +++ b/waterfox/browser/components/privatetab/test/browser/browser_privatetab.js @@ -1,5 +1,3 @@ -"use strict"; - requestLongerTimeout(2); async function togglePrivate(tab, skip = false) { @@ -7,32 +5,32 @@ async function togglePrivate(tab, skip = false) { return PrivateTab.togglePrivate(window, tab); } await openTabContextMenu(tab); - let openPrivate = document.getElementById("toggleTabPrivateState"); + const openPrivate = document.getElementById("toggleTabPrivateState"); openPrivate.click(); return gBrowser.selectedTab; } // Test elements exist in correct locations add_task(async function testButtonsExist() { - let b1 = document.getElementById("openAllPrivate"); + const b1 = document.getElementById("openAllPrivate"); ok(b1, "Multiple bookmark context menu item added."); - let b2 = document.getElementById("openAllLinksPrivate"); + const b2 = document.getElementById("openAllLinksPrivate"); ok(b2, "Multiple link context menu item added."); - let b3 = document.getElementById("openPrivate"); + const b3 = document.getElementById("openPrivate"); ok(b3, "New private tab item added."); - let b4 = document.getElementById("menu_newPrivateTab"); + const b4 = document.getElementById("menu_newPrivateTab"); ok(b4, "Menu item added."); - let b5 = document.getElementById("openLinkInPrivateTab"); + const b5 = document.getElementById("openLinkInPrivateTab"); ok(b5, "Link context menu item added."); - let b6 = document.getElementById("toggleTabPrivateState"); + const b6 = document.getElementById("toggleTabPrivateState"); ok(b6, "Tab context menu item added."); }); // Test container exists add_task(async function testContainer() { ContextualIdentityService.ensureDataReady(); - let container = ContextualIdentityService._identities.find( - c => c.name == "Private" + const container = ContextualIdentityService._identities.find( + (c) => c.name === "Private" ); ok(container, "Found Private container."); }); @@ -58,8 +56,8 @@ add_task(async function testAutofillNotStored() { "http://mochi.test:8888/browser/browser/components/" + "sessionstore/test/browser_formdata_sample.html"; - const OUTER_VALUE = "browser_formdata_" + Math.random(); - const INNER_VALUE = "browser_formdata_" + Math.random(); + const OUTER_VALUE = `browser_formdata_${Math.random()}`; + const INNER_VALUE = `browser_formdata_${Math.random()}`; // Creates a tab, loads a page with some form fields, // modifies their values and closes the tab. @@ -68,7 +66,7 @@ add_task(async function testAutofillNotStored() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); // Toggle to a private tab tab = await togglePrivate(tab, true); - let browser = tab.linkedBrowser; + const browser = tab.linkedBrowser; await promiseBrowserLoaded(browser); // Modify form data. @@ -85,7 +83,7 @@ add_task(async function testAutofillNotStored() { } await createAndRemoveTab(); - let [ + const [ { state: { formdata }, }, @@ -99,7 +97,7 @@ add_task(async function testTabHistoryNotStored() { let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI1); // Make it private tab = await togglePrivate(tab, true); - let browser = tab.linkedBrowser; + const browser = tab.linkedBrowser; await promiseBrowserLoaded(browser); // Open a new URL BrowserTestUtils.loadURI(browser, URI2); @@ -107,14 +105,14 @@ add_task(async function testTabHistoryNotStored() { // Remove tab to save state BrowserTestUtils.removeTab(tab); // Verify only non-private data stored - let closedTabData = JSON.parse(SessionStore.getClosedTabData(window)).filter( - data => { + const closedTabData = JSON.parse(SessionStore.getClosedTabData(window)).filter( + (data) => { return ( data.state.entries[0].url === URI1 || data.state.entries[0].url === URI2 ); } ); - let privateData = closedTabData.filter(data => { + const privateData = closedTabData.filter((data) => { return data.state.isPrivate === true; }); const oneClosedTabWithNoPrivateData = @@ -140,12 +138,12 @@ const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; async function getSuggestionResults() { await UrlbarTestUtils.promiseSearchComplete(window); - let results = []; - let matchCount = UrlbarTestUtils.getResultCount(window); + const results = []; + const matchCount = UrlbarTestUtils.getResultCount(window); for (let i = 0; i < matchCount; i++) { - let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); if ( - result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.type === UrlbarUtils.RESULT_TYPE.SEARCH && result.searchParams.suggestion ) { result.index = i; @@ -157,15 +155,15 @@ async function getSuggestionResults() { // Must run first. add_task(async function prepare() { - let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + const suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); - let engine = await SearchTestUtils.promiseNewSearchEngine( + const engine = await SearchTestUtils.promiseNewSearchEngine( getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME ); - let oldDefaultEngine = await Services.search.getDefault(); + const oldDefaultEngine = await Services.search.getDefault(); await Services.search.setDefault(engine); await UrlbarTestUtils.formHistory.clear(); - registerCleanupFunction(async function() { + registerCleanupFunction(async () => { Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); await Services.search.setDefault(oldDefaultEngine); @@ -184,7 +182,7 @@ add_task(async function testSearchSuggestionsNotStored() { window, value: "foo", }); - let results = await getSuggestionResults(); + const results = await getSuggestionResults(); ok(!results.length, "Suggestion not be stored in private tab"); // Cleanup BrowserTestUtils.removeTab(tab); diff --git a/waterfox/browser/components/privatetab/test/browser/head.js b/waterfox/browser/components/privatetab/test/browser/head.js index a2fadc1271b7..5867dcee2315 100644 --- a/waterfox/browser/components/privatetab/test/browser/head.js +++ b/waterfox/browser/components/privatetab/test/browser/head.js @@ -1,37 +1,33 @@ -"use strict"; - -const { AppConstants } = ChromeUtils.import( - "resource://gre/modules/AppConstants.jsm" +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" ); -const { TabStateFlusher } = ChromeUtils.import( - "resource:///modules/sessionstore/TabStateFlusher.jsm" +const { TabStateCache } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateCache.sys.mjs" ); -const { TabStateCache } = ChromeUtils.import( - "resource:///modules/sessionstore/TabStateCache.jsm" -); - -const { SearchTestUtils } = ChromeUtils.import( - "resource://testing-common/SearchTestUtils.jsm" +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" ); SearchTestUtils.init(this); -const { UrlbarTestUtils } = ChromeUtils.import( - "resource://testing-common/UrlbarTestUtils.jsm" +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" ); UrlbarTestUtils.init(this); -const { PrivateTab } = ChromeUtils.import("resource:///modules/PrivateTab.jsm"); +const { PrivateTab } = ChromeUtils.importESModule( + "resource:///modules/PrivateTab.sys.mjs" +); -const URI1 = "https://test1.example.com/"; -const URI2 = "https://example.com/"; +const _URI1 = "https://test1.example.com/"; +const _URI2 = "https://example.com/"; -let OS = AppConstants.platform; +const _OS = AppConstants.platform; -function promiseBrowserLoaded( +function _promiseBrowserLoaded( aBrowser, ignoreSubFrames = true, wantLoad = null @@ -41,21 +37,21 @@ function promiseBrowserLoaded( // Removes the given tab immediately and returns a promise that resolves when // all pending status updates (messages) of the closing tab have been received. -function promiseRemoveTabAndSessionState(tab) { - let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); +function _promiseRemoveTabAndSessionState(tab) { + const sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); BrowserTestUtils.removeTab(tab); return sessionUpdatePromise; } -function setPropertyOfFormField(browserContext, selector, propName, newValue) { +function _setPropertyOfFormField(browserContext, selector, propName, newValue) { return SpecialPowers.spawn( browserContext, [selector, propName, newValue], (selectorChild, propNameChild, newValueChild) => { - let node = content.document.querySelector(selectorChild); + const node = content.document.querySelector(selectorChild); node[propNameChild] = newValueChild; - let event = node.ownerDocument.createEvent("UIEvents"); + const event = node.ownerDocument.createEvent("UIEvents"); event.initUIEvent("input", true, true, node.ownerGlobal, 0); node.dispatchEvent(event); } @@ -65,10 +61,10 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) { /** * Helper for opening the toolbar context menu. */ -async function openTabContextMenu(tab) { +async function _openTabContextMenu(tab) { info("Opening tab context menu"); - let contextMenu = document.getElementById("tabContextMenu"); - let openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent( + const contextMenu = document.getElementById("tabContextMenu"); + const openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent( contextMenu, "shown" );