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="placesContext_openBookmarkLinks:tabs" />
+ insertafter="placesContext_open:newtab" />
+ insertafter="placesContext_openLinks:tabs" />
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"
);