1251 lines
40 KiB
JavaScript
1251 lines
40 KiB
JavaScript
/* 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
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|