fune/browser/base/content/tabbrowser.js
Cristina Horotan 5f4356e527 Backed out 9 changesets (bug 1810141) for several test failures on a CLOSED TREE
Backed out changeset 8781a0d1254d (bug 1810141)
Backed out changeset 131037295784 (bug 1810141)
Backed out changeset 3852fbe290f4 (bug 1810141)
Backed out changeset 118f131a524a (bug 1810141)
Backed out changeset ab5d76846e10 (bug 1810141)
Backed out changeset dce3aa683445 (bug 1810141)
Backed out changeset 4dc41d90dbb3 (bug 1810141)
Backed out changeset 50b57ba1a061 (bug 1810141)
Backed out changeset 569de94781e4 (bug 1810141)
2023-02-13 16:05:30 +02:00

7350 lines
232 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* 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/. */
{
// start private scope for gBrowser
/**
* A set of known icons to use for internal pages. These are hardcoded so we can
* start loading them faster than ContentLinkHandler would normally find them.
*/
const FAVICON_DEFAULTS = {
"about:newtab": "chrome://branding/content/icon32.png",
"about:home": "chrome://branding/content/icon32.png",
"about:welcome": "chrome://branding/content/icon32.png",
"about:privatebrowsing":
"chrome://browser/skin/privatebrowsing/favicon.svg",
};
window._gBrowser = {
init() {
ChromeUtils.defineModuleGetter(
this,
"AsyncTabSwitcher",
"resource:///modules/AsyncTabSwitcher.jsm"
);
ChromeUtils.defineESModuleGetters(this, {
UrlbarProviderOpenTabs:
"resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetters(this, {
MacSharingService: [
"@mozilla.org/widget/macsharingservice;1",
"nsIMacSharingService",
],
});
XPCOMUtils.defineLazyGetter(this, "tabLocalization", () => {
return new Localization(
["browser/tabbrowser.ftl", "branding/brand.ftl"],
true
);
});
if (AppConstants.MOZ_CRASHREPORTER) {
ChromeUtils.defineModuleGetter(
this,
"TabCrashHandler",
"resource:///modules/ContentCrashHandlers.jsm"
);
}
Services.obs.addObserver(this, "contextual-identity-updated");
Services.els.addSystemEventListener(document, "keydown", this, false);
Services.els.addSystemEventListener(document, "keypress", this, false);
document.addEventListener("visibilitychange", this);
window.addEventListener("framefocusrequested", this);
this.tabContainer.init();
this._setupInitialBrowserAndTab();
if (
Services.prefs.getIntPref("browser.display.document_color_use") == 2
) {
this.tabpanels.style.backgroundColor = Services.prefs.getBoolPref(
"browser.display.use_system_colors"
)
? "canvas"
: Services.prefs.getCharPref("browser.display.background_color");
}
this._setFindbarData();
// We take over setting the document title, so remove the l10n id to
// avoid it being re-translated and overwriting document content if
// we ever switch languages at runtime. After a language change, the
// window title will update at the next tab or location change.
document.querySelector("title").removeAttribute("data-l10n-id");
this._setupEventListeners();
this._initialized = true;
},
ownerGlobal: window,
ownerDocument: document,
closingTabsEnum: {
ALL: 0,
OTHER: 1,
TO_START: 2,
TO_END: 3,
MULTI_SELECTED: 4,
},
_lastRelatedTabMap: new WeakMap(),
mProgressListeners: [],
mTabsProgressListeners: [],
_tabListeners: new Map(),
_tabFilters: new Map(),
_isBusy: false,
_awaitingToggleCaretBrowsingPrompt: false,
arrowKeysShouldWrap: AppConstants == "macosx",
_dateTimePicker: null,
_previewMode: false,
_lastFindValue: "",
_contentWaitingCount: 0,
_tabLayerCache: [],
tabAnimationsInProgress: 0,
/**
* Binding from browser to tab
*/
_tabForBrowser: new WeakMap(),
/**
* `_createLazyBrowser` will define properties on the unbound lazy browser
* which correspond to properties defined in MozBrowser which will be bound to
* the browser when it is inserted into the document. If any of these
* properties are accessed by consumers, `_insertBrowser` is called and
* the browser is inserted to ensure that things don't break. This list
* provides the names of properties that may be called while the browser
* is in its unbound (lazy) state.
*/
_browserBindingProperties: [
"canGoBack",
"canGoForward",
"goBack",
"goForward",
"permitUnload",
"reload",
"reloadWithFlags",
"stop",
"loadURI",
"gotoIndex",
"currentURI",
"documentURI",
"remoteType",
"preferences",
"imageDocument",
"isRemoteBrowser",
"messageManager",
"getTabBrowser",
"finder",
"fastFind",
"sessionHistory",
"contentTitle",
"characterSet",
"fullZoom",
"textZoom",
"tabHasCustomZoom",
"webProgress",
"addProgressListener",
"removeProgressListener",
"audioPlaybackStarted",
"audioPlaybackStopped",
"resumeMedia",
"mute",
"unmute",
"blockedPopups",
"lastURI",
"purgeSessionHistory",
"stopScroll",
"startScroll",
"userTypedValue",
"userTypedClear",
"didStartLoadSinceLastUserTyping",
"audioMuted",
],
_removingTabs: new Set(),
_multiSelectedTabsSet: new WeakSet(),
_lastMultiSelectedTabRef: null,
_clearMultiSelectionLocked: false,
_clearMultiSelectionLockedOnce: false,
_multiSelectChangeStarted: false,
_multiSelectChangeAdditions: new Set(),
_multiSelectChangeRemovals: new Set(),
_multiSelectChangeSelected: false,
/**
* Tab close requests are ignored if the window is closing anyway,
* e.g. when holding Ctrl+W.
*/
_windowIsClosing: false,
preloadedBrowser: null,
/**
* This defines a proxy which allows us to access browsers by
* index without actually creating a full array of browsers.
*/
browsers: new Proxy([], {
has: (target, name) => {
if (typeof name == "string" && Number.isInteger(parseInt(name))) {
return name in gBrowser.tabs;
}
return false;
},
get: (target, name) => {
if (name == "length") {
return gBrowser.tabs.length;
}
if (typeof name == "string" && Number.isInteger(parseInt(name))) {
if (!(name in gBrowser.tabs)) {
return undefined;
}
return gBrowser.tabs[name].linkedBrowser;
}
return target[name];
},
}),
/**
* List of browsers whose docshells must be active in order for print preview
* to work.
*/
_printPreviewBrowsers: new Set(),
_switcher: null,
_soundPlayingAttrRemovalTimer: 0,
_hoverTabTimer: null,
_featureCallout: null,
_featureCalloutPanelId: null,
get tabContainer() {
delete this.tabContainer;
return (this.tabContainer = document.getElementById("tabbrowser-tabs"));
},
get tabs() {
return this.tabContainer.allTabs;
},
get tabbox() {
delete this.tabbox;
return (this.tabbox = document.getElementById("tabbrowser-tabbox"));
},
get tabpanels() {
delete this.tabpanels;
return (this.tabpanels = document.getElementById("tabbrowser-tabpanels"));
},
addEventListener(...args) {
this.tabpanels.addEventListener(...args);
},
removeEventListener(...args) {
this.tabpanels.removeEventListener(...args);
},
dispatchEvent(...args) {
return this.tabpanels.dispatchEvent(...args);
},
get visibleTabs() {
return this.tabContainer._getVisibleTabs();
},
get _numPinnedTabs() {
for (var i = 0; i < this.tabs.length; i++) {
if (!this.tabs[i].pinned) {
break;
}
}
return i;
},
set selectedTab(val) {
if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
document.documentElement.hasAttribute("window-modal-open") ||
(gNavToolbox.collapsed && !this._allowTabChange)
) {
return;
}
// Update the tab
this.tabbox.selectedTab = val;
},
get selectedTab() {
return this._selectedTab;
},
get selectedBrowser() {
return this._selectedBrowser;
},
get featureCallout() {
return this._featureCallout;
},
set featureCallout(val) {
this._featureCallout = val;
},
get instantiateFeatureCalloutTour() {
return this._instantiateFeatureCalloutTour;
},
get featureCalloutPanelId() {
return this._featureCalloutPanelId;
},
_instantiateFeatureCalloutTour(browser, panelId) {
this._featureCalloutPanelId = panelId;
const { FeatureCallout } = ChromeUtils.importESModule(
"resource:///modules/FeatureCallout.sys.mjs"
);
// Note - once we have additional browser chrome messages,
// only use PDF.js pref value when navigating to PDF viewer
this._featureCallout = new FeatureCallout({
win: window,
browser,
prefName: "browser.pdfjs.feature-tour",
page: "chrome",
});
},
_setupInitialBrowserAndTab() {
// See browser.js for the meaning of window.arguments.
// Bug 1485961 covers making this more sane.
let userContextId = window.arguments && window.arguments[5];
let openWindowInfo = window.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow).initialOpenWindowInfo;
if (!openWindowInfo && window.arguments && window.arguments[11]) {
openWindowInfo = window.arguments[11];
}
let tabArgument = gBrowserInit.getTabToAdopt();
// If we have a tab argument with browser, we use its remoteType. Otherwise,
// if e10s is disabled or there's a parent process opener (e.g. parent
// process about: page) for the content tab, we use a parent
// process remoteType. Otherwise, we check the URI to determine
// what to do - if there isn't one, we default to the default remote type.
//
// When adopting a tab, we'll also use that tab's browsingContextGroupId,
// if available, to ensure we don't spawn a new process.
let remoteType;
let initialBrowsingContextGroupId;
if (tabArgument && tabArgument.hasAttribute("usercontextid")) {
// The window's first argument is a tab if and only if we are swapping tabs.
// We must set the browser's usercontextid so that the newly created remote
// tab child has the correct usercontextid.
userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
}
if (tabArgument && tabArgument.linkedBrowser) {
remoteType = tabArgument.linkedBrowser.remoteType;
initialBrowsingContextGroupId =
tabArgument.linkedBrowser.browsingContext?.group.id;
} else if (openWindowInfo) {
userContextId = openWindowInfo.originAttributes.userContextId;
if (openWindowInfo.isRemote) {
remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
} else {
remoteType = E10SUtils.NOT_REMOTE;
}
} else {
let uriToLoad = gBrowserInit.uriToLoadPromise;
if (uriToLoad && Array.isArray(uriToLoad)) {
uriToLoad = uriToLoad[0]; // we only care about the first item
}
if (uriToLoad && typeof uriToLoad == "string") {
let oa = E10SUtils.predictOriginAttributes({
window,
userContextId,
});
remoteType = E10SUtils.getRemoteTypeForURI(
uriToLoad,
gMultiProcessBrowser,
gFissionBrowser,
E10SUtils.DEFAULT_REMOTE_TYPE,
null,
oa
);
} else {
// If we reach here, we don't have the url to load. This means that
// `uriToLoad` is most likely a promise which is waiting on SessionStore
// initialization. We can't delay setting up the browser here, as that
// would mean that `gBrowser.selectedBrowser` might not always exist,
// which is the current assumption.
// In this case we default to the privileged about process as that's
// the best guess we can make, and we'll likely need it eventually.
remoteType = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
}
}
let createOptions = {
uriIsAboutBlank: false,
userContextId,
initialBrowsingContextGroupId,
remoteType,
openWindowInfo,
};
let browser = this.createBrowser(createOptions);
browser.setAttribute("primary", "true");
if (gBrowserAllowScriptsToCloseInitialTabs) {
browser.setAttribute("allowscriptstoclose", "true");
}
browser.droppedLinkHandler = handleDroppedLink;
browser.loadURI = _loadURI.bind(null, browser);
let uniqueId = this._generateUniquePanelID();
let panel = this.getPanel(browser);
panel.id = uniqueId;
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
tab.permanentKey = browser.permanentKey;
tab._tPos = 0;
tab._fullyOpen = true;
tab.linkedBrowser = browser;
if (userContextId) {
tab.setAttribute("usercontextid", userContextId);
ContextualIdentityService.setTabStyle(tab);
}
this._tabForBrowser.set(browser, tab);
this._appendStatusPanel();
// This is the initial browser, so it's usually active; the default is false
// so we have to update it:
browser.docShellIsActive = this.shouldActivateDocShell(browser);
// Hook the browser up with a progress listener.
let tabListener = new TabProgressListener(tab, browser, true, false);
let filter = Cc[
"@mozilla.org/appshell/component/browser-status-filter;1"
].createInstance(Ci.nsIWebProgress);
filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
this._tabListeners.set(tab, tabListener);
this._tabFilters.set(tab, filter);
browser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
},
/**
* BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
* MAKE SURE TO ADD IT HERE AS WELL.
*/
get canGoBack() {
return this.selectedBrowser.canGoBack;
},
get canGoForward() {
return this.selectedBrowser.canGoForward;
},
goBack(requireUserInteraction) {
return this.selectedBrowser.goBack(requireUserInteraction);
},
goForward(requireUserInteraction) {
return this.selectedBrowser.goForward(requireUserInteraction);
},
reload() {
return this.selectedBrowser.reload();
},
reloadWithFlags(aFlags) {
return this.selectedBrowser.reloadWithFlags(aFlags);
},
stop() {
return this.selectedBrowser.stop();
},
/**
* throws exception for unknown schemes
*/
loadURI(aURI, aParams) {
return this.selectedBrowser.loadURI(aURI, aParams);
},
gotoIndex(aIndex) {
return this.selectedBrowser.gotoIndex(aIndex);
},
get currentURI() {
return this.selectedBrowser.currentURI;
},
get finder() {
return this.selectedBrowser.finder;
},
get docShell() {
return this.selectedBrowser.docShell;
},
get webNavigation() {
return this.selectedBrowser.webNavigation;
},
get webProgress() {
return this.selectedBrowser.webProgress;
},
get contentWindow() {
return this.selectedBrowser.contentWindow;
},
get sessionHistory() {
return this.selectedBrowser.sessionHistory;
},
get markupDocumentViewer() {
return this.selectedBrowser.markupDocumentViewer;
},
get contentDocument() {
return this.selectedBrowser.contentDocument;
},
get contentTitle() {
return this.selectedBrowser.contentTitle;
},
get contentPrincipal() {
return this.selectedBrowser.contentPrincipal;
},
get securityUI() {
return this.selectedBrowser.securityUI;
},
set fullZoom(val) {
this.selectedBrowser.fullZoom = val;
},
get fullZoom() {
return this.selectedBrowser.fullZoom;
},
set textZoom(val) {
this.selectedBrowser.textZoom = val;
},
get textZoom() {
return this.selectedBrowser.textZoom;
},
get isSyntheticDocument() {
return this.selectedBrowser.isSyntheticDocument;
},
set userTypedValue(val) {
this.selectedBrowser.userTypedValue = val;
},
get userTypedValue() {
return this.selectedBrowser.userTypedValue;
},
_setFindbarData() {
// Ensure we know what the find bar key is in the content process:
let { sharedData } = Services.ppmm;
if (!sharedData.has("Findbar:Shortcut")) {
let keyEl = document.getElementById("key_find");
let mods = keyEl
.getAttribute("modifiers")
.replace(
/accel/i,
AppConstants.platform == "macosx" ? "meta" : "control"
);
sharedData.set("Findbar:Shortcut", {
key: keyEl.getAttribute("key"),
shiftKey: mods.includes("shift"),
ctrlKey: mods.includes("control"),
altKey: mods.includes("alt"),
metaKey: mods.includes("meta"),
});
}
},
isFindBarInitialized(aTab) {
return (aTab || this.selectedTab)._findBar != undefined;
},
/**
* Get the already constructed findbar
*/
getCachedFindBar(aTab = this.selectedTab) {
return aTab._findBar;
},
/**
* Get the findbar, and create it if it doesn't exist.
* @return the find bar (or null if the window or tab is closed/closing in the interim).
*/
async getFindBar(aTab = this.selectedTab) {
let findBar = this.getCachedFindBar(aTab);
if (findBar) {
return findBar;
}
// Avoid re-entrancy by caching the promise we're about to return.
if (!aTab._pendingFindBar) {
aTab._pendingFindBar = this._createFindBar(aTab);
}
return aTab._pendingFindBar;
},
/**
* Create a findbar instance.
* @param aTab the tab to create the find bar for.
* @return the created findbar, or null if the window or tab is closed/closing.
*/
async _createFindBar(aTab) {
let findBar = document.createXULElement("findbar");
let browser = this.getBrowserForTab(aTab);
browser.parentNode.insertAdjacentElement("afterend", findBar);
await new Promise(r => requestAnimationFrame(r));
delete aTab._pendingFindBar;
if (window.closed || aTab.closing) {
return null;
}
findBar.browser = browser;
findBar._findField.value = this._lastFindValue;
aTab._findBar = findBar;
let event = document.createEvent("Events");
event.initEvent("TabFindInitialized", true, false);
aTab.dispatchEvent(event);
return findBar;
},
_appendStatusPanel() {
this.selectedBrowser.insertAdjacentElement("afterend", StatusPanel.panel);
},
_updateTabBarForPinnedTabs() {
this.tabContainer._unlockTabSizing();
this.tabContainer._positionPinnedTabs();
this.tabContainer._setPositionalAttributes();
this.tabContainer._updateCloseButtons();
},
_notifyPinnedStatus(aTab) {
aTab.linkedBrowser.sendMessageToActor(
"Browser:AppTab",
{ isAppTab: aTab.pinned },
"BrowserTab"
);
let event = document.createEvent("Events");
event.initEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", true, false);
aTab.dispatchEvent(event);
},
pinTab(aTab) {
if (aTab.pinned) {
return;
}
this.showTab(aTab);
this.moveTabTo(aTab, this._numPinnedTabs);
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
this._notifyPinnedStatus(aTab);
},
unpinTab(aTab) {
if (!aTab.pinned) {
return;
}
this.moveTabTo(aTab, this._numPinnedTabs - 1);
aTab.removeAttribute("pinned");
aTab.style.marginInlineStart = "";
aTab._pinnedUnscrollable = false;
this._updateTabBarForPinnedTabs();
this._notifyPinnedStatus(aTab);
},
previewTab(aTab, aCallback) {
let currentTab = this.selectedTab;
try {
// Suppress focus, ownership and selected tab changes
this._previewMode = true;
this.selectedTab = aTab;
aCallback();
} finally {
this.selectedTab = currentTab;
this._previewMode = false;
}
},
_getAndMaybeCreateDateTimePickerPanel() {
if (!this._dateTimePicker) {
let wrapper = document.getElementById("dateTimePickerTemplate");
wrapper.replaceWith(wrapper.content);
this._dateTimePicker = document.getElementById("DateTimePickerPanel");
}
return this._dateTimePicker;
},
syncThrobberAnimations(aTab) {
aTab.ownerGlobal.promiseDocumentFlushed(() => {
if (!aTab.container) {
return;
}
const animations = Array.from(
aTab.container.getElementsByTagName("tab")
)
.map(tab => {
const throbber = tab.throbber;
return throbber ? throbber.getAnimations({ subtree: true }) : [];
})
.reduce((a, b) => a.concat(b))
.filter(
anim =>
CSSAnimation.isInstance(anim) &&
(anim.animationName === "tab-throbber-animation" ||
anim.animationName === "tab-throbber-animation-rtl") &&
anim.playState === "running"
);
// Synchronize with the oldest running animation, if any.
const firstStartTime = Math.min(
...animations.map(anim =>
anim.startTime === null ? Infinity : anim.startTime
)
);
if (firstStartTime === Infinity) {
return;
}
requestAnimationFrame(() => {
for (let animation of animations) {
// If |animation| has been cancelled since this rAF callback
// was scheduled we don't want to set its startTime since
// that would restart it. We check for a cancelled animation
// by looking for a null currentTime rather than checking
// the playState, since reading the playState of
// a CSSAnimation object will flush style.
if (animation.currentTime !== null) {
animation.startTime = firstStartTime;
}
}
});
});
},
getBrowserAtIndex(aIndex) {
return this.browsers[aIndex];
},
getBrowserForOuterWindowID(aID) {
for (let b of this.browsers) {
if (b.outerWindowID == aID) {
return b;
}
}
return null;
},
getTabForBrowser(aBrowser) {
return this._tabForBrowser.get(aBrowser);
},
getPanel(aBrowser) {
return this.getBrowserContainer(aBrowser).parentNode;
},
getBrowserContainer(aBrowser) {
return (aBrowser || this.selectedBrowser).parentNode.parentNode;
},
getTabNotificationDeck() {
if (!this._tabNotificationDeck) {
let template = document.getElementById(
"tab-notification-deck-template"
);
template.replaceWith(template.content);
this._tabNotificationDeck = document.getElementById(
"tab-notification-deck"
);
}
return this._tabNotificationDeck;
},
_nextNotificationBoxId: 0,
getNotificationBox(aBrowser) {
let browser = aBrowser || this.selectedBrowser;
if (!browser._notificationBox) {
browser._notificationBox = new MozElements.NotificationBox(element => {
element.setAttribute("notificationside", "top");
element.setAttribute(
"name",
`tab-notification-box-${this._nextNotificationBoxId++}`
);
this.getTabNotificationDeck().append(element);
if (browser == this.selectedBrowser) {
this._updateVisibleNotificationBox(browser);
}
});
}
return browser._notificationBox;
},
readNotificationBox(aBrowser) {
let browser = aBrowser || this.selectedBrowser;
return browser._notificationBox || null;
},
_updateVisibleNotificationBox(aBrowser) {
if (!this._tabNotificationDeck) {
// If the deck hasn't been created we don't need to create it here.
return;
}
let notificationBox = this.readNotificationBox(aBrowser);
this.getTabNotificationDeck().selectedViewName = notificationBox
? notificationBox.stack.getAttribute("name")
: "";
},
getTabModalPromptBox(aBrowser) {
let browser = aBrowser || this.selectedBrowser;
if (!browser.tabModalPromptBox) {
browser.tabModalPromptBox = new TabModalPromptBox(browser);
}
return browser.tabModalPromptBox;
},
getTabDialogBox(aBrowser) {
if (!aBrowser) {
throw new Error("aBrowser is required");
}
if (!aBrowser.tabDialogBox) {
aBrowser.tabDialogBox = new TabDialogBox(aBrowser);
}
return aBrowser.tabDialogBox;
},
getTabFromAudioEvent(aEvent) {
if (!aEvent.isTrusted) {
return null;
}
var browser = aEvent.originalTarget;
var tab = this.getTabForBrowser(browser);
return tab;
},
_callProgressListeners(
aBrowser,
aMethod,
aArguments,
aCallGlobalListeners = true,
aCallTabsListeners = true
) {
var rv = true;
function callListeners(listeners, args) {
for (let p of listeners) {
if (aMethod in p) {
try {
if (!p[aMethod].apply(p, args)) {
rv = false;
}
} catch (e) {
// don't inhibit other listeners
console.error(e);
}
}
}
}
aBrowser = aBrowser || this.selectedBrowser;
if (aCallGlobalListeners && aBrowser == this.selectedBrowser) {
callListeners(this.mProgressListeners, aArguments);
}
if (aCallTabsListeners) {
aArguments.unshift(aBrowser);
callListeners(this.mTabsProgressListeners, aArguments);
}
return rv;
},
/**
* Sets an icon for the tab if the URI is defined in FAVICON_DEFAULTS.
*/
setDefaultIcon(aTab, aURI) {
if (aURI && aURI.spec in FAVICON_DEFAULTS) {
this.setIcon(aTab, FAVICON_DEFAULTS[aURI.spec]);
}
},
setIcon(
aTab,
aIconURL = "",
aOriginalURL = aIconURL,
aLoadingPrincipal = null
) {
let makeString = url => (url instanceof Ci.nsIURI ? url.spec : url);
aIconURL = makeString(aIconURL);
aOriginalURL = makeString(aOriginalURL);
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
if (
aIconURL &&
!aLoadingPrincipal &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
) {
console.error(
`Attempt to set a remote URL ${aIconURL} as a tab icon without a loading principal.`
);
return;
}
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
if (aIconURL != aTab.getAttribute("image")) {
if (aIconURL) {
if (aLoadingPrincipal) {
aTab.setAttribute("iconloadingprincipal", aLoadingPrincipal);
} else {
aTab.removeAttribute("iconloadingprincipal");
}
aTab.setAttribute("image", aIconURL);
} else {
aTab.removeAttribute("image");
aTab.removeAttribute("iconloadingprincipal");
}
this._tabAttrModified(aTab, ["image"]);
}
// The aOriginalURL argument is currently only used by tests.
this._callProgressListeners(browser, "onLinkIconAvailable", [
aIconURL,
aOriginalURL,
]);
},
getIcon(aTab) {
let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
return browser.mIconURL;
},
setPageInfo(aURL, aDescription, aPreviewImage) {
if (aURL) {
let pageInfo = {
url: aURL,
description: aDescription,
previewImageURL: aPreviewImage,
};
PlacesUtils.history.update(pageInfo).catch(console.error);
}
},
getWindowTitleForBrowser(aBrowser) {
let docElement = document.documentElement;
let title = "";
// If location bar is hidden and the URL type supports a host,
// add the scheme and host to the title to prevent spoofing.
// XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
try {
if (docElement.getAttribute("chromehidden").includes("location")) {
const uri = Services.io.createExposableURI(aBrowser.currentURI);
let prefix = uri.prePath;
if (uri.scheme == "about") {
prefix = uri.spec;
} else if (uri.scheme == "moz-extension") {
const ext = WebExtensionPolicy.getByHostname(uri.host);
if (ext && ext.name) {
let extensionLabel = document.getElementById(
"urlbar-label-extension"
);
prefix = `${extensionLabel.value} (${ext.name})`;
}
}
title = prefix + " - ";
}
} catch (e) {
// ignored
}
if (docElement.hasAttribute("titlepreface")) {
title += docElement.getAttribute("titlepreface");
}
let tab = this.getTabForBrowser(aBrowser);
if (tab._labelIsContentTitle) {
// Strip out any null bytes in the content title, since the
// underlying widget implementations of nsWindow::SetTitle pass
// null-terminated strings to system APIs.
title += tab.getAttribute("label").replace(/\0/g, "");
}
let dataSuffix =
docElement.getAttribute("privatebrowsingmode") == "temporary"
? "Private"
: "Default";
if (title) {
// We're using a function rather than just using `title` as the
// new substring to avoid `$$`, `$'` etc. having a special
// meaning to `replace`.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter
// and the documentation for functions for more info about this.
return docElement.dataset["contentTitle" + dataSuffix].replace(
"CONTENTTITLE",
() => title
);
}
return docElement.dataset["title" + dataSuffix];
},
updateTitlebar() {
document.title = this.getWindowTitleForBrowser(this.selectedBrowser);
},
updateCurrentBrowser(aForceUpdate) {
let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
if (this.selectedBrowser == newBrowser && !aForceUpdate) {
return;
}
let newTab = this.getTabForBrowser(newBrowser);
if (
this._featureCallout &&
this._featureCalloutPanelId !== newTab.linkedPanel
) {
this._featureCallout.endTour(true);
this._featureCallout = null;
}
// For now, only check for Feature Callout messages
// when viewing PDFs. Later, we can expand this to check
// for callout messages on every change of tab location.
if (
!this._featureCallout &&
newBrowser.contentPrincipal.originNoSuffix === "resource://pdf.js"
) {
this._instantiateFeatureCalloutTour(newBrowser, newTab.linkedPanel);
window.gBrowser.featureCallout.showFeatureCallout();
}
if (!aForceUpdate) {
TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
if (gMultiProcessBrowser) {
this._asyncTabSwitching = true;
this._getSwitcher().requestTab(newTab);
this._asyncTabSwitching = false;
}
document.commandDispatcher.lock();
}
let oldTab = this.selectedTab;
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
oldTab.owner = null;
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
if (lastRelatedTab) {
if (!lastRelatedTab.selected) {
lastRelatedTab.owner = null;
}
}
this._lastRelatedTabMap = new WeakMap();
let oldBrowser = this.selectedBrowser;
if (!gMultiProcessBrowser) {
oldBrowser.removeAttribute("primary");
oldBrowser.docShellIsActive = false;
newBrowser.setAttribute("primary", "true");
newBrowser.docShellIsActive = !document.hidden;
}
this._selectedBrowser = newBrowser;
this._selectedTab = newTab;
this.showTab(newTab);
this._appendStatusPanel();
this._updateVisibleNotificationBox(newBrowser);
let oldBrowserPopupsBlocked = oldBrowser.popupBlocker.getBlockedPopupCount();
let newBrowserPopupsBlocked = newBrowser.popupBlocker.getBlockedPopupCount();
if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) {
newBrowser.popupBlocker.updateBlockedPopupsUI();
}
// Update the URL bar.
let webProgress = newBrowser.webProgress;
this._callProgressListeners(
null,
"onLocationChange",
[webProgress, null, newBrowser.currentURI, 0, true],
true,
false
);
let securityUI = newBrowser.securityUI;
if (securityUI) {
this._callProgressListeners(
null,
"onSecurityChange",
[webProgress, null, securityUI.state],
true,
false
);
// Include the true final argument to indicate that this event is
// simulated (instead of being observed by the webProgressListener).
this._callProgressListeners(
null,
"onContentBlockingEvent",
[webProgress, null, newBrowser.getContentBlockingEvents(), true],
true,
false
);
}
let listener = this._tabListeners.get(newTab);
if (listener && listener.mStateFlags) {
this._callProgressListeners(
null,
"onUpdateCurrentBrowser",
[
listener.mStateFlags,
listener.mStatus,
listener.mMessage,
listener.mTotalProgress,
],
true,
false
);
}
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
oldTab.updateLastAccessed();
let oldFindBar = oldTab._findBar;
if (
oldFindBar &&
oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
!oldFindBar.hidden
) {
this._lastFindValue = oldFindBar._findField.value;
}
this.updateTitlebar();
newTab.removeAttribute("titlechanged");
newTab.attention = false;
// The tab has been selected, it's not unselected anymore.
// (1) Call the current tab's finishUnselectedTabHoverTimer()
// to save a telemetry record.
// (2) Call the current browser's unselectedTabHover() with false
// to dispatch an event.
newTab.finishUnselectedTabHoverTimer();
newBrowser.unselectedTabHover(false);
}
// If the new tab is busy, and our current state is not busy, then
// we need to fire a start to all progress listeners.
if (newTab.hasAttribute("busy") && !this._isBusy) {
this._isBusy = true;
this._callProgressListeners(
null,
"onStateChange",
[
webProgress,
null,
Ci.nsIWebProgressListener.STATE_START |
Ci.nsIWebProgressListener.STATE_IS_NETWORK,
0,
],
true,
false
);
}
// If the new tab is not busy, and our current state is busy, then
// we need to fire a stop to all progress listeners.
if (!newTab.hasAttribute("busy") && this._isBusy) {
this._isBusy = false;
this._callProgressListeners(
null,
"onStateChange",
[
webProgress,
null,
Ci.nsIWebProgressListener.STATE_STOP |
Ci.nsIWebProgressListener.STATE_IS_NETWORK,
0,
],
true,
false
);
}
// TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
// that might rely upon the other changes suppressed.
// Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
if (!this._previewMode) {
// We've selected the new tab, so go ahead and notify listeners.
let event = new CustomEvent("TabSelect", {
bubbles: true,
cancelable: false,
detail: {
previousTab: oldTab,
},
});
newTab.dispatchEvent(event);
this._tabAttrModified(oldTab, ["selected"]);
this._tabAttrModified(newTab, ["selected"]);
this.readNotificationBox(newBrowser)?.shown();
this._startMultiSelectChange();
this._multiSelectChangeSelected = true;
this.clearMultiSelectedTabs();
if (this._multiSelectChangeAdditions.size) {
// Some tab has been multiselected just before switching tabs.
// The tab that was selected at that point should also be multiselected.
this.addToMultiSelectedTabs(oldTab);
}
if (oldBrowser != newBrowser && oldBrowser.getInPermitUnload) {
oldBrowser.getInPermitUnload(inPermitUnload => {
if (!inPermitUnload) {
return;
}
// Since the user is switching away from a tab that has
// a beforeunload prompt active, we remove the prompt.
// This prevents confusing user flows like the following:
// 1. User attempts to close Firefox
// 2. User switches tabs (ingoring a beforeunload prompt)
// 3. User returns to tab, presses "Leave page"
let promptBox = this.getTabModalPromptBox(oldBrowser);
let prompts = promptBox.listPrompts();
// There might not be any prompts here if the tab was closed
// while in an onbeforeunload prompt, which will have
// destroyed aforementioned prompt already, so check there's
// something to remove, first:
if (prompts.length) {
// NB: This code assumes that the beforeunload prompt
// is the top-most prompt on the tab.
prompts[prompts.length - 1].abortPrompt();
}
});
}
if (!gMultiProcessBrowser) {
this._adjustFocusBeforeTabSwitch(oldTab, newTab);
this._adjustFocusAfterTabSwitch(newTab);
gURLBar.afterTabSwitchFocusChange();
}
}
updateUserContextUIIndicator();
gPermissionPanel.updateSharingIndicator();
// Enable touch events to start a native dragging
// session to allow the user to easily drag the selected tab.
// This is currently only supported on Windows.
oldTab.removeAttribute("touchdownstartsdrag");
newTab.setAttribute("touchdownstartsdrag", "true");
if (!gMultiProcessBrowser) {
this.tabContainer._setPositionalAttributes();
document.commandDispatcher.unlock();
let event = new CustomEvent("TabSwitchDone", {
bubbles: true,
cancelable: true,
});
this.dispatchEvent(event);
}
if (!aForceUpdate) {
TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
}
},
_adjustFocusBeforeTabSwitch(oldTab, newTab) {
if (this._previewMode) {
return;
}
let oldBrowser = oldTab.linkedBrowser;
let newBrowser = newTab.linkedBrowser;
oldBrowser._urlbarFocused = gURLBar && gURLBar.focused;
if (this.isFindBarInitialized(oldTab)) {
let findBar = this.getCachedFindBar(oldTab);
oldTab._findBarFocused =
!findBar.hidden &&
findBar._findField.getAttribute("focused") == "true";
}
let activeEl = document.activeElement;
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
} else if (
gMultiProcessBrowser &&
activeEl != newBrowser &&
activeEl != newTab
) {
// In e10s, if focus isn't already in the tabstrip or on the new browser,
// and the new browser's previous focus wasn't in the url bar but focus is
// there now, we need to adjust focus further.
let keepFocusOnUrlBar =
newBrowser && newBrowser._urlbarFocused && gURLBar && gURLBar.focused;
if (!keepFocusOnUrlBar) {
// Clear focus so that _adjustFocusAfterTabSwitch can detect if
// some element has been focused and respect that.
document.activeElement.blur();
}
}
},
_adjustFocusAfterTabSwitch(newTab) {
// Don't steal focus from the tab bar.
if (document.activeElement == newTab) {
return;
}
let newBrowser = this.getBrowserForTab(newTab);
if (newBrowser.hasAttribute("tabDialogShowing")) {
newBrowser.tabDialogBox.focus();
return;
}
if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
// If there's a tabmodal prompt showing, focus it.
let prompts = newBrowser.tabModalPromptBox.listPrompts();
let prompt = prompts[prompts.length - 1];
// @tabmodalPromptShowing is also set for other tab modal prompts
// (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>.
// Bug 1492814 will implement this for the Payment Request dialog.
if (prompt) {
prompt.Dialog.setDefaultFocus();
return;
}
}
// Focus the location bar if it was previously focused for that tab.
// In full screen mode, only bother making the location bar visible
// if the tab is a blank one.
if (newBrowser._urlbarFocused && gURLBar) {
// If the user happened to type into the URL bar for this browser
// by the time we got here, focusing will cause the text to be
// selected which could cause them to overwrite what they've
// already typed in.
if (gURLBar.focused && newBrowser.userTypedValue) {
return;
}
let selectURL = () => {
if (this._asyncTabSwitching) {
// Set _awaitingSetURI flag to suppress popup notification
// explicitly while tab switching asynchronously.
newBrowser._awaitingSetURI = true;
// The onLocationChange event called in updateCurrentBrowser() will
// be captured in browser.js, then it calls gURLBar.setURI(). In case
// of that doing processing of here before doing above processing,
// the selection status that gURLBar.select() does will be releasing
// by gURLBar.setURI(). To resolve it, we call gURLBar.select() after
// finishing gURLBar.setURI().
const currentActiveElement = document.activeElement;
gURLBar.inputField.addEventListener(
"SetURI",
() => {
if (currentActiveElement === document.activeElement) {
gURLBar.select();
}
delete newBrowser._awaitingSetURI;
},
{ once: true }
);
} else {
gURLBar.select();
}
};
// This inDOMFullscreen attribute indicates that the page has something
// such as a video in fullscreen mode. Opening a new tab will cancel
// fullscreen mode, so we need to wait for that to happen and then
// select the url field.
if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
window.addEventListener("MozDOMFullscreen:Exited", selectURL, {
once: true,
wantsUntrusted: false,
});
return;
}
if (!window.fullScreen || newTab.isEmpty) {
selectURL();
return;
}
}
// Focus the find bar if it was previously focused for that tab.
if (
gFindBarInitialized &&
!gFindBar.hidden &&
this.selectedTab._findBarFocused
) {
gFindBar._findField.focus();
return;
}
// Don't focus the content area if something has been focused after the
// tab switch was initiated.
if (gMultiProcessBrowser && document.activeElement != document.body) {
return;
}
// We're now committed to focusing the content area.
let fm = Services.focus;
let focusFlags = fm.FLAG_NOSCROLL;
if (!gMultiProcessBrowser) {
let newFocusedElement = fm.getFocusedElementForWindow(
window.content,
true,
{}
);
// for anchors, use FLAG_SHOWRING so that it is clear what link was
// last clicked when switching back to that tab
if (
newFocusedElement &&
(HTMLAnchorElement.isInstance(newFocusedElement) ||
newFocusedElement.getAttributeNS(
"http://www.w3.org/1999/xlink",
"type"
) == "simple")
) {
focusFlags |= fm.FLAG_SHOWRING;
}
}
fm.setFocus(newBrowser, focusFlags);
},
_tabAttrModified(aTab, aChanged) {
if (aTab.closing) {
return;
}
let event = new CustomEvent("TabAttrModified", {
bubbles: true,
cancelable: false,
detail: {
changed: aChanged,
},
});
aTab.dispatchEvent(event);
},
resetBrowserSharing(aBrowser) {
let tab = this.getTabForBrowser(aBrowser);
if (!tab) {
return;
}
// If WebRTC was used, leave object to enable tracking of grace periods.
tab._sharingState = tab._sharingState?.webRTC ? { webRTC: {} } : {};
tab.removeAttribute("sharing");
this._tabAttrModified(tab, ["sharing"]);
if (aBrowser == this.selectedBrowser) {
gPermissionPanel.updateSharingIndicator();
}
},
updateBrowserSharing(aBrowser, aState) {
let tab = this.getTabForBrowser(aBrowser);
if (!tab) {
return;
}
if (tab._sharingState == null) {
tab._sharingState = {};
}
tab._sharingState = Object.assign(tab._sharingState, aState);
if ("webRTC" in aState) {
if (tab._sharingState.webRTC?.sharing) {
if (tab._sharingState.webRTC.paused) {
tab.removeAttribute("sharing");
} else {
tab.setAttribute("sharing", aState.webRTC.sharing);
}
} else {
tab.removeAttribute("sharing");
}
this._tabAttrModified(tab, ["sharing"]);
}
if (aBrowser == this.selectedBrowser) {
gPermissionPanel.updateSharingIndicator();
}
},
getTabSharingState(aTab) {
// Normalize the state object for consumers (ie.extensions).
let state = Object.assign(
{},
aTab._sharingState && aTab._sharingState.webRTC
);
return {
camera: !!state.camera,
microphone: !!state.microphone,
screen: state.screen && state.screen.replace("Paused", ""),
};
},
setInitialTabTitle(aTab, aTitle, aOptions = {}) {
// Convert some non-content title (actually a url) to human readable title
if (!aOptions.isContentTitle && isBlankPageURL(aTitle)) {
aTitle = this.tabContainer.emptyTabTitle;
}
if (aTitle) {
if (!aTab.getAttribute("label")) {
aTab._labelIsInitialTitle = true;
}
this._setTabLabel(aTab, aTitle, aOptions);
}
},
_dataURLRegEx: /^data:[^,]+;base64,/i,
setTabTitle(aTab) {
var browser = this.getBrowserForTab(aTab);
var title = browser.contentTitle;
if (aTab.hasAttribute("customizemode")) {
title = this.tabLocalization.formatValueSync(
"tabbrowser-customizemode-tab-title"
);
}
// Don't replace an initially set label with the URL while the tab
// is loading.
if (aTab._labelIsInitialTitle) {
if (!title) {
return false;
}
delete aTab._labelIsInitialTitle;
}
let isURL = false;
let isContentTitle = !!title;
if (!title) {
// See if we can use the URI as the title.
if (browser.currentURI.displaySpec) {
try {
title = Services.io.createExposableURI(browser.currentURI)
.displaySpec;
} catch (ex) {
title = browser.currentURI.displaySpec;
}
}
if (title && !isBlankPageURL(title)) {
isURL = true;
if (title.length <= 500 || !this._dataURLRegEx.test(title)) {
// Try to unescape not-ASCII URIs using the current character set.
try {
let characterSet = browser.characterSet;
title = Services.textToSubURI.unEscapeNonAsciiURI(
characterSet,
title
);
} catch (ex) {
/* Do nothing. */
}
}
} else {
// No suitable URI? Fall back to our untitled string.
title = this.tabContainer.emptyTabTitle;
}
}
return this._setTabLabel(aTab, title, { isContentTitle, isURL });
},
// While an auth prompt from a base domain different than the current sites is open, we do not want to show the tab title of the current site,
// but of the origin that is requesting authentication.
// This is to prevent possible auth spoofing scenarios.
// See bug 791594 for reference.
setTabLabelForAuthPrompts(aTab, aLabel) {
return this._setTabLabel(aTab, aLabel);
},
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
if (!aLabel || aLabel.includes("about:reader?")) {
return false;
}
// If it's a long data: URI that uses base64 encoding, truncate to a
// reasonable length rather than trying to display the entire thing,
// which can hang or crash the browser.
// We can't shorten arbitrary URIs like this, as bidi etc might mean
// we need the trailing characters for display. But a base64-encoded
// data-URI is plain ASCII, so this is OK for tab-title display.
// (See bug 1408854.)
if (isURL && aLabel.length > 500 && this._dataURLRegEx.test(aLabel)) {
aLabel = aLabel.substring(0, 500) + "\u2026";
}
aTab._fullLabel = aLabel;
if (!isContentTitle) {
// Remove protocol and "www."
if (!("_regex_shortenURLForTabLabel" in this)) {
this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
}
aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
}
aTab._labelIsContentTitle = isContentTitle;
if (aTab.getAttribute("label") == aLabel) {
return false;
}
let dwu = window.windowUtils;
let isRTL =
dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
aTab.setAttribute("label", aLabel);
aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
aTab.toggleAttribute("labelendaligned", isRTL != (document.dir == "rtl"));
// Dispatch TabAttrModified event unless we're setting the label
// before the TabOpen event was dispatched.
if (!beforeTabOpen) {
this._tabAttrModified(aTab, ["label"]);
}
if (aTab.selected) {
this.updateTitlebar();
}
return true;
},
loadTabs(
aURIs,
{
allowInheritPrincipal,
allowThirdPartyFixup,
inBackground,
newIndex,
postDatas,
replace,
targetTab,
triggeringPrincipal,
csp,
userContextId,
fromExternal,
} = {}
) {
if (!aURIs.length) {
return;
}
// The tab selected after this new tab is closed (i.e. the new tab's
// "owner") is the next adjacent tab (i.e. not the previously viewed tab)
// when several urls are opened here (i.e. closing the first should select
// the next of many URLs opened) or if the pref to have UI links opened in
// the background is set (i.e. the link is not being opened modally)
//
// i.e.
// Number of URLs Load UI Links in BG Focus Last Viewed?
// == 1 false YES
// == 1 true NO
// > 1 false/true NO
var multiple = aURIs.length > 1;
var owner = multiple || inBackground ? null : this.selectedTab;
var firstTabAdded = null;
var targetTabIndex = -1;
if (typeof newIndex != "number") {
newIndex = -1;
}
// When bulk opening tabs, such as from a bookmark folder, we want to insertAfterCurrent
// if necessary, but we also will set the bulkOrderedOpen flag so that the bookmarks
// open in the same order they are in the folder.
if (
multiple &&
newIndex < 0 &&
Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")
) {
newIndex = this.selectedTab._tPos + 1;
}
if (replace) {
let browser;
if (targetTab) {
browser = this.getBrowserForTab(targetTab);
targetTabIndex = targetTab._tPos;
} else {
browser = this.selectedBrowser;
targetTabIndex = this.tabContainer.selectedIndex;
}
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (allowThirdPartyFixup) {
flags |=
Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
}
if (!allowInheritPrincipal) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
}
if (fromExternal) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
}
try {
browser.loadURI(aURIs[0], {
flags,
postData: postDatas && postDatas[0],
triggeringPrincipal,
csp,
});
} catch (e) {
// Ignore failure in case a URI is wrong, so we can continue
// opening the next ones.
}
} else {
let params = {
allowInheritPrincipal,
ownerTab: owner,
skipAnimation: multiple,
allowThirdPartyFixup,
postData: postDatas && postDatas[0],
userContextId,
triggeringPrincipal,
bulkOrderedOpen: multiple,
csp,
fromExternal,
};
if (newIndex > -1) {
params.index = newIndex;
}
firstTabAdded = this.addTab(aURIs[0], params);
if (newIndex > -1) {
targetTabIndex = firstTabAdded._tPos;
}
}
let tabNum = targetTabIndex;
for (let i = 1; i < aURIs.length; ++i) {
let params = {
allowInheritPrincipal,
skipAnimation: true,
allowThirdPartyFixup,
postData: postDatas && postDatas[i],
userContextId,
triggeringPrincipal,
bulkOrderedOpen: true,
csp,
fromExternal,
};
if (targetTabIndex > -1) {
params.index = ++tabNum;
}
this.addTab(aURIs[i], params);
}
if (firstTabAdded && !inBackground) {
this.selectedTab = firstTabAdded;
}
},
updateBrowserRemoteness(aBrowser, { newFrameloader, remoteType } = {}) {
let isRemote = aBrowser.getAttribute("remote") == "true";
// We have to be careful with this here, as the "no remote type" is null,
// not a string. Make sure to check only for undefined, since null is
// allowed.
if (remoteType === undefined) {
throw new Error("Remote type must be set!");
}
let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
if (!gMultiProcessBrowser && shouldBeRemote) {
throw new Error(
"Cannot switch to remote browser in a window " +
"without the remote tabs load context."
);
}
// Abort if we're not going to change anything
let oldRemoteType = aBrowser.remoteType;
if (
isRemote == shouldBeRemote &&
!newFrameloader &&
(!isRemote || oldRemoteType == remoteType)
) {
return false;
}
let tab = this.getTabForBrowser(aBrowser);
// aBrowser needs to be inserted now if it hasn't been already.
this._insertBrowser(tab);
let evt = document.createEvent("Events");
evt.initEvent("BeforeTabRemotenessChange", true, false);
tab.dispatchEvent(evt);
let wasActive = document.activeElement == aBrowser;
// Unhook our progress listener.
let filter = this._tabFilters.get(tab);
let listener = this._tabListeners.get(tab);
aBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(listener);
// We'll be creating a new listener, so destroy the old one.
listener.destroy();
let oldDroppedLinkHandler = aBrowser.droppedLinkHandler;
let oldUserTypedValue = aBrowser.userTypedValue;
let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
// Change the "remote" attribute.
// Make sure the browser is destroyed so it unregisters from observer notifications
aBrowser.destroy();
if (shouldBeRemote) {
aBrowser.setAttribute("remote", "true");
aBrowser.setAttribute("remoteType", remoteType);
} else {
aBrowser.setAttribute("remote", "false");
aBrowser.removeAttribute("remoteType");
}
// This call actually switches out our frameloaders. Do this as late as
// possible before rebuilding the browser, as we'll need the new browser
// state set up completely first.
aBrowser.changeRemoteness({
remoteType,
});
// Once we have new frameloaders, this call sets the browser back up.
aBrowser.construct();
aBrowser.userTypedValue = oldUserTypedValue;
if (hadStartedLoad) {
aBrowser.urlbarChangeTracker.startedLoad();
}
aBrowser.droppedLinkHandler = oldDroppedLinkHandler;
// This shouldn't really be necessary, however, this has the side effect
// of sending MozLayerTreeReady / MozLayerTreeCleared events for remote
// frames, which the tab switcher depends on.
//
// eslint-disable-next-line no-self-assign
aBrowser.docShellIsActive = aBrowser.docShellIsActive;
// Create a new tab progress listener for the new browser we just injected,
// since tab progress listeners have logic for handling the initial about:blank
// load
listener = new TabProgressListener(tab, aBrowser, true, false);
this._tabListeners.set(tab, listener);
filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
// Restore the progress listener.
aBrowser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
// Restore the securityUI state.
let securityUI = aBrowser.securityUI;
let state = securityUI
? securityUI.state
: Ci.nsIWebProgressListener.STATE_IS_INSECURE;
this._callProgressListeners(
aBrowser,
"onSecurityChange",
[aBrowser.webProgress, null, state],
true,
false
);
let event = aBrowser.getContentBlockingEvents();
// Include the true final argument to indicate that this event is
// simulated (instead of being observed by the webProgressListener).
this._callProgressListeners(
aBrowser,
"onContentBlockingEvent",
[aBrowser.webProgress, null, event, true],
true,
false
);
if (shouldBeRemote) {
// Switching the browser to be remote will connect to a new child
// process so the browser can no longer be considered to be
// crashed.
tab.removeAttribute("crashed");
// we call updatetabIndicatorAttr here, rather than _tabAttrModified, so as
// to be consistent with how "crashed" attribute changes are handled elsewhere
this.tabContainer.updateTabIndicatorAttr(tab);
} else {
aBrowser.sendMessageToActor(
"Browser:AppTab",
{ isAppTab: tab.pinned },
"BrowserTab"
);
}
if (wasActive) {
aBrowser.focus();
}
// If the findbar has been initialised, reset its browser reference.
if (this.isFindBarInitialized(tab)) {
this.getCachedFindBar(tab).browser = aBrowser;
}
tab.linkedBrowser.sendMessageToActor(
"Browser:HasSiblings",
this.tabs.length > 1,
"BrowserTab"
);
evt = document.createEvent("Events");
evt.initEvent("TabRemotenessChange", true, false);
tab.dispatchEvent(evt);
return true;
},
updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) {
if (!gMultiProcessBrowser) {
return this.updateBrowserRemoteness(aBrowser, {
remoteType: E10SUtils.NOT_REMOTE,
});
}
let oldRemoteType = aBrowser.remoteType;
let oa = E10SUtils.predictOriginAttributes({ browser: aBrowser });
aOptions.remoteType = E10SUtils.getRemoteTypeForURI(
aURL,
gMultiProcessBrowser,
gFissionBrowser,
oldRemoteType,
aBrowser.currentURI,
oa
);
// If this URL can't load in the current browser then flip it to the
// correct type.
if (oldRemoteType != aOptions.remoteType || aOptions.newFrameloader) {
return this.updateBrowserRemoteness(aBrowser, aOptions);
}
return false;
},
createBrowser({
isPreloadBrowser,
name,
openWindowInfo,
remoteType,
initialBrowsingContextGroupId,
uriIsAboutBlank,
userContextId,
skipLoad,
initiallyActive,
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
// permanentKey is held by something after this window closes, it
// doesn't keep the window alive.
b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
// Ensure that SessionStore has flushed any session history state from the
// content process before we this browser's remoteness.
if (!Services.appinfo.sessionHistoryInParent) {
b.prepareToChangeRemoteness = () =>
SessionStore.prepareToChangeRemoteness(b);
b.afterChangeRemoteness = switchId => {
let tab = this.getTabForBrowser(b);
SessionStore.finishTabRemotenessChange(tab, switchId);
return true;
};
}
const defaultBrowserAttributes = {
contextmenu: "contentAreaContextMenu",
message: "true",
messagemanagergroup: "browsers",
tooltip: "aHTMLTooltip",
type: "content",
};
for (let attribute in defaultBrowserAttributes) {
b.setAttribute(attribute, defaultBrowserAttributes[attribute]);
}
if (gMultiProcessBrowser || remoteType) {
b.setAttribute("maychangeremoteness", "true");
}
if (!initiallyActive) {
b.setAttribute("initiallyactive", "false");
}
if (userContextId) {
b.setAttribute("usercontextid", userContextId);
}
if (remoteType) {
b.setAttribute("remoteType", remoteType);
b.setAttribute("remote", "true");
}
if (!isPreloadBrowser) {
b.setAttribute("autocompletepopup", "PopupAutoComplete");
}
/*
* This attribute is meant to describe if the browser is the
* preloaded browser. When the preloaded browser is created, the
* 'preloadedState' attribute for that browser is set to "preloaded", and
* when a new tab is opened, and it is time to show that preloaded
* browser, the 'preloadedState' attribute for that browser is removed.
*
* See more details on Bug 1420285.
*/
if (isPreloadBrowser) {
b.setAttribute("preloadedState", "preloaded");
}
// Ensure that the browser will be created in a specific initial
// BrowsingContextGroup. This may change the process selection behaviour
// of the newly created browser, and is often used in combination with
// "remoteType" to ensure that the initial about:blank load occurs
// within the same process as another window.
if (initialBrowsingContextGroupId) {
b.setAttribute(
"initialBrowsingContextGroupId",
initialBrowsingContextGroupId
);
}
// Propagate information about the opening content window to the browser.
if (openWindowInfo) {
b.openWindowInfo = openWindowInfo;
}
// This will be used by gecko to control the name of the opened
// window.
if (name) {
// XXX: The `name` property is special in HTML and XUL. Should
// we use a different attribute name for this?
b.setAttribute("name", name);
}
let notificationbox = document.createXULElement("notificationbox");
notificationbox.setAttribute("notificationside", "top");
let stack = document.createXULElement("stack");
stack.className = "browserStack";
stack.appendChild(b);
let browserContainer = document.createXULElement("vbox");
browserContainer.className = "browserContainer";
browserContainer.appendChild(notificationbox);
browserContainer.appendChild(stack);
let browserSidebarContainer = document.createXULElement("hbox");
browserSidebarContainer.className = "browserSidebarContainer";
browserSidebarContainer.appendChild(browserContainer);
// Prevent the superfluous initial load of a blank document
// if we're going to load something other than about:blank.
if (!uriIsAboutBlank || skipLoad) {
b.setAttribute("nodefaultsrc", "true");
}
return b;
},
_createLazyBrowser(aTab) {
let browser = aTab.linkedBrowser;
let names = this._browserBindingProperties;
for (let i = 0; i < names.length; i++) {
let name = names[i];
let getter;
let setter;
switch (name) {
case "audioMuted":
getter = () => aTab.hasAttribute("muted");
break;
case "contentTitle":
getter = () => SessionStore.getLazyTabValue(aTab, "title");
break;
case "currentURI":
getter = () => {
// Avoid recreating the same nsIURI object over and over again...
if (browser._cachedCurrentURI) {
return browser._cachedCurrentURI;
}
let url =
SessionStore.getLazyTabValue(aTab, "url") || "about:blank";
return (browser._cachedCurrentURI = Services.io.newURI(url));
};
break;
case "didStartLoadSinceLastUserTyping":
getter = () => () => false;
break;
case "fullZoom":
case "textZoom":
getter = () => 1;
break;
case "tabHasCustomZoom":
getter = () => false;
break;
case "getTabBrowser":
getter = () => () => this;
break;
case "isRemoteBrowser":
getter = () => browser.getAttribute("remote") == "true";
break;
case "permitUnload":
getter = () => () => ({ permitUnload: true });
break;
case "reload":
case "reloadWithFlags":
getter = () => params => {
// Wait for load handler to be instantiated before
// initializing the reload.
aTab.addEventListener(
"SSTabRestoring",
() => {
browser[name](params);
},
{ once: true }
);
gBrowser._insertBrowser(aTab);
};
break;
case "remoteType":
getter = () => {
let url =
SessionStore.getLazyTabValue(aTab, "url") || "about:blank";
// Avoid recreating the same nsIURI object over and over again...
let uri;
if (browser._cachedCurrentURI) {
uri = browser._cachedCurrentURI;
} else {
uri = browser._cachedCurrentURI = Services.io.newURI(url);
}
let oa = E10SUtils.predictOriginAttributes({
browser,
userContextId: aTab.getAttribute("usercontextid"),
});
return E10SUtils.getRemoteTypeForURI(
url,
gMultiProcessBrowser,
gFissionBrowser,
undefined,
uri,
oa
);
};
break;
case "userTypedValue":
case "userTypedClear":
getter = () => SessionStore.getLazyTabValue(aTab, name);
break;
default:
getter = () => {
if (AppConstants.NIGHTLY_BUILD) {
let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
Services.console.logStringMessage(message + new Error().stack);
}
this._insertBrowser(aTab);
return browser[name];
};
setter = value => {
if (AppConstants.NIGHTLY_BUILD) {
let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
Services.console.logStringMessage(message + new Error().stack);
}
this._insertBrowser(aTab);
return (browser[name] = value);
};
}
Object.defineProperty(browser, name, {
get: getter,
set: setter,
configurable: true,
enumerable: true,
});
}
},
_insertBrowser(aTab, aInsertedOnTabCreation) {
"use strict";
// If browser is already inserted or window is closed don't do anything.
if (aTab.linkedPanel || window.closed) {
return;
}
let browser = aTab.linkedBrowser;
// If browser is a lazy browser, delete the substitute properties.
if (this._browserBindingProperties[0] in browser) {
for (let name of this._browserBindingProperties) {
delete browser[name];
}
}
let { uriIsAboutBlank, usingPreloadedContent } = aTab._browserParams;
delete aTab._browserParams;
delete browser._cachedCurrentURI;
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
panel.id = uniqueId;
aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary.
if (!panel.parentNode) {
// NB: this appendChild call causes us to run constructors for the
// browser element, which fires off a bunch of notifications. Some
// of those notifications can cause code to run that inspects our
// state, so it is important that the tab element is fully
// initialized by this point.
this.tabpanels.appendChild(panel);
}
// wire up a progress listener for the new browser object.
let tabListener = new TabProgressListener(
aTab,
browser,
uriIsAboutBlank,
usingPreloadedContent
);
const filter = Cc[
"@mozilla.org/appshell/component/browser-status-filter;1"
].createInstance(Ci.nsIWebProgress);
filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
browser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
this._tabListeners.set(aTab, tabListener);
this._tabFilters.set(aTab, filter);
browser.droppedLinkHandler = handleDroppedLink;
browser.loadURI = _loadURI.bind(null, browser);
// Most of the time, we start our browser's docShells out as inactive,
// and then maintain activeness in the tab switcher. Preloaded about:newtab's
// are already created with their docShell's as inactive, but then explicitly
// render their layers to ensure that we can switch to them quickly. We avoid
// setting docShellIsActive to false again in this case, since that'd cause
// the layers for the preloaded tab to be dropped, and we'd see a flash
// of empty content instead.
//
// So for all browsers except for the preloaded case, we set the browser
// docShell to inactive.
if (!usingPreloadedContent) {
browser.docShellIsActive = false;
}
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
this.tabs[0].linkedBrowser.sendMessageToActor(
"Browser:HasSiblings",
true,
"BrowserTab"
);
this.tabs[1].linkedBrowser.sendMessageToActor(
"Browser:HasSiblings",
true,
"BrowserTab"
);
} else {
aTab.linkedBrowser.sendMessageToActor(
"Browser:HasSiblings",
this.tabs.length > 1,
"BrowserTab"
);
}
// Only fire this event if the tab is already in the DOM
// and will be handled by a listener.
if (aTab.isConnected) {
var evt = new CustomEvent("TabBrowserInserted", {
bubbles: true,
detail: { insertedOnTabCreation: aInsertedOnTabCreation },
});
aTab.dispatchEvent(evt);
}
},
_mayDiscardBrowser(aTab, aForceDiscard) {
let browser = aTab.linkedBrowser;
let action = aForceDiscard ? "unload" : "dontUnload";
if (
!aTab ||
aTab.selected ||
aTab.closing ||
this._windowIsClosing ||
!browser.isConnected ||
!browser.isRemoteBrowser ||
!browser.permitUnload(action).permitUnload
) {
return false;
}
return true;
},
discardBrowser(aTab, aForceDiscard) {
"use strict";
let browser = aTab.linkedBrowser;
if (!this._mayDiscardBrowser(aTab, aForceDiscard)) {
return false;
}
// Reset sharing state.
if (aTab._sharingState) {
this.resetBrowserSharing(browser);
}
webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext);
// Set browser parameters for when browser is restored. Also remove
// listeners and set up lazy restore data in SessionStore. This must
// be done before browser is destroyed and removed from the document.
aTab._browserParams = {
uriIsAboutBlank: browser.currentURI.spec == "about:blank",
remoteType: browser.remoteType,
usingPreloadedContent: false,
};
SessionStore.resetBrowserToLazyState(aTab);
// Remove the tab's filter and progress listener.
let filter = this._tabFilters.get(aTab);
let listener = this._tabListeners.get(aTab);
browser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(listener);
listener.destroy();
this._tabListeners.delete(aTab);
this._tabFilters.delete(aTab);
// Reset the findbar and remove it if it is attached to the tab.
if (aTab._findBar) {
aTab._findBar.close(true);
aTab._findBar.remove();
delete aTab._findBar;
}
// Remove potentially stale attributes.
let attributesToRemove = [
"activemedia-blocked",
"busy",
"pendingicon",
"progress",
"soundplaying",
];
let removedAttributes = [];
for (let attr of attributesToRemove) {
if (aTab.hasAttribute(attr)) {
removedAttributes.push(attr);
aTab.removeAttribute(attr);
}
}
if (removedAttributes.length) {
this._tabAttrModified(aTab, removedAttributes);
}
browser.destroy();
this.getPanel(browser).remove();
aTab.removeAttribute("linkedpanel");
this._createLazyBrowser(aTab);
let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
aTab.dispatchEvent(evt);
return true;
},
/**
* Loads a tab with a default null principal unless specified
*/
addWebTab(aURI, params = {}) {
if (!params.triggeringPrincipal) {
params.triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal(
{
userContextId: params.userContextId,
}
);
}
if (params.triggeringPrincipal.isSystemPrincipal) {
throw new Error(
"System principal should never be passed into addWebTab()"
);
}
return this.addTab(aURI, params);
},
addAdjacentNewTab(tab) {
Services.obs.notifyObservers(
{
wrappedJSObject: new Promise(resolve => {
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
index: tab._tPos + 1,
userContextId: tab.userContextId,
});
resolve(this.selectedBrowser);
}),
},
"browser-open-newtab-start"
);
},
/**
* Must only be used sparingly for content that came from Chrome context
* If in doubt use addWebTab
*/
addTrustedTab(aURI, params = {}) {
params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
return this.addTab(aURI, params);
},
/**
* @returns {object}
* The new tab. The return value will be null if the tab couldn't be
* created; this shouldn't normally happen, and an error will be logged
* to the console if it does.
*/
// eslint-disable-next-line complexity
addTab(
aURI,
{
allowInheritPrincipal,
allowThirdPartyFixup,
bulkOrderedOpen,
charset,
createLazyBrowser,
disableTRR,
eventDetail,
focusUrlBar,
forceNotRemote,
forceAllowDataURI,
fromExternal,
inBackground = true,
index,
lazyTabTitle,
name,
noInitialLabel,
openWindowInfo,
openerBrowser,
originPrincipal,
originStoragePrincipal,
ownerTab,
pinned,
postData,
preferredRemoteType,
referrerInfo,
relatedToCurrent,
initialBrowsingContextGroupId,
skipAnimation,
skipBackgroundNotify,
triggeringPrincipal,
userContextId,
csp,
skipLoad = createLazyBrowser,
batchInsertingTabs,
globalHistoryOptions,
triggeringRemoteType,
} = {}
) {
// all callers of addTab that pass a params object need to pass
// a valid triggeringPrincipal.
if (!triggeringPrincipal) {
throw new Error(
"Required argument triggeringPrincipal missing within addTab"
);
}
if (!UserInteraction.running("browser.tabs.opening", window)) {
UserInteraction.start("browser.tabs.opening", "initting", window);
}
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
// Don't use document.l10n.setAttributes because the FTL file is loaded
// lazily and we won't be able to resolve the string.
document
.getElementById("History:UndoCloseTab")
.setAttribute("data-l10n-args", JSON.stringify({ tabCount: 1 }));
// if we're adding tabs, we're past interrupt mode, ditch the owner
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
// user context ID.
//
// If we have a browser opener (which is usually the browser
// element from a remote window.open() call), use that.
//
// Otherwise, if the tab is related to the current tab (e.g.,
// because it was opened by a link click), use the selected tab as
// the owner. If referrerInfo is set, and we don't have an
// explicit relatedToCurrent arg, we assume that the tab is
// related to the current tab, since referrerURI is null or
// undefined if the tab is opened from an external application or
// bookmark (i.e. somewhere other than an existing tab).
if (relatedToCurrent == null) {
relatedToCurrent = !!(referrerInfo && referrerInfo.originalReferrer);
}
let openerTab =
(openerBrowser && this.getTabForBrowser(openerBrowser)) ||
(relatedToCurrent && this.selectedTab);
var t = document.createXULElement("tab", { is: "tabbrowser-tab" });
// Tag the tab as being created so extension code can ignore events
// prior to TabOpen.
t.initializingTab = true;
t.openerTab = openerTab;
aURI = aURI || "about:blank";
let aURIObject = null;
try {
aURIObject = Services.io.newURI(aURI);
} catch (ex) {
/* we'll try to fix up this URL later */
}
let lazyBrowserURI;
if (createLazyBrowser && aURI != "about:blank") {
lazyBrowserURI = aURIObject;
aURI = "about:blank";
}
var uriIsAboutBlank = aURI == "about:blank";
// When overflowing, new tabs are scrolled into view smoothly, which
// doesn't go well together with the width transition. So we skip the
// transition in that case.
let animate =
!skipAnimation &&
!pinned &&
this.tabContainer.getAttribute("overflow") != "true" &&
!gReduceMotion;
// Related tab inherits current tab's user context unless a different
// usercontextid is specified
if (userContextId == null && openerTab) {
userContextId = openerTab.getAttribute("usercontextid") || 0;
}
if (!noInitialLabel) {
if (isBlankPageURL(aURI)) {
t.setAttribute("label", this.tabContainer.emptyTabTitle);
} else {
// Set URL as label so that the tab isn't empty initially.
this.setInitialTabTitle(t, aURI, {
beforeTabOpen: true,
isURL: true,
});
}
}
if (userContextId) {
t.setAttribute("usercontextid", userContextId);
ContextualIdentityService.setTabStyle(t);
}
if (skipBackgroundNotify) {
t.setAttribute("skipbackgroundnotify", true);
}
if (pinned) {
t.setAttribute("pinned", "true");
}
t.classList.add("tabbrowser-tab");
this.tabContainer._unlockTabSizing();
if (!animate) {
UserInteraction.update("browser.tabs.opening", "not-animated", window);
t.setAttribute("fadein", "true");
// Call _handleNewTab asynchronously as it needs to know if the
// new tab is selected.
setTimeout(
function(tabContainer) {
tabContainer._handleNewTab(t);
},
0,
this.tabContainer
);
} else {
UserInteraction.update("browser.tabs.opening", "animated", window);
}
let usingPreloadedContent = false;
let b;
try {
if (!batchInsertingTabs) {
// When we are not restoring a session, we need to know
// insert the tab into the tab container in the correct position
this._insertTabAtIndex(t, {
index,
ownerTab,
openerTab,
pinned,
bulkOrderedOpen,
});
}
// If we don't have a preferred remote type, and we have a remote
// opener, use the opener's remote type.
if (!preferredRemoteType && openerBrowser) {
preferredRemoteType = openerBrowser.remoteType;
}
var oa = E10SUtils.predictOriginAttributes({ window, userContextId });
// If URI is about:blank and we don't have a preferred remote type,
// then we need to use the referrer, if we have one, to get the
// correct remote type for the new tab.
if (
uriIsAboutBlank &&
!preferredRemoteType &&
referrerInfo &&
referrerInfo.originalReferrer
) {
preferredRemoteType = E10SUtils.getRemoteTypeForURI(
referrerInfo.originalReferrer.spec,
gMultiProcessBrowser,
gFissionBrowser,
E10SUtils.DEFAULT_REMOTE_TYPE,
null,
oa
);
}
let remoteType = forceNotRemote
? E10SUtils.NOT_REMOTE
: E10SUtils.getRemoteTypeForURI(
aURI,
gMultiProcessBrowser,
gFissionBrowser,
preferredRemoteType,
null,
oa
);
// If we open a new tab with the newtab URL in the default
// userContext, check if there is a preloaded browser ready.
if (aURI == BROWSER_NEW_TAB_URL && !userContextId) {
b = NewTabPagePreloading.getPreloadedBrowser(window);
if (b) {
usingPreloadedContent = true;
}
}
if (!b) {
// No preloaded browser found, create one.
b = this.createBrowser({
remoteType,
uriIsAboutBlank,
userContextId,
initialBrowsingContextGroupId,
openWindowInfo,
name,
skipLoad,
});
}
t.linkedBrowser = b;
if (focusUrlBar) {
b._urlbarFocused = true;
}
this._tabForBrowser.set(b, t);
t.permanentKey = b.permanentKey;
t._browserParams = {
uriIsAboutBlank,
remoteType,
usingPreloadedContent,
};
// If the caller opts in, create a lazy browser.
if (createLazyBrowser) {
this._createLazyBrowser(t);
if (lazyBrowserURI) {
// Lazy browser must be explicitly registered so tab will appear as
// a switch-to-tab candidate in autocomplete.
this.UrlbarProviderOpenTabs.registerOpenTab(
lazyBrowserURI.spec,
userContextId || 0,
PrivateBrowsingUtils.isWindowPrivate(window)
);
b.registeredOpenURI = lazyBrowserURI;
}
SessionStore.setTabState(t, {
entries: [
{
url: lazyBrowserURI ? lazyBrowserURI.spec : "about:blank",
title: lazyTabTitle,
triggeringPrincipal_base64: E10SUtils.serializePrincipal(
triggeringPrincipal
),
},
],
});
} else {
this._insertBrowser(t, true);
// If we were called by frontend and don't have openWindowInfo,
// but we were opened from another browser, set the cross group
// opener ID:
if (openerBrowser && !openWindowInfo) {
b.browsingContext.setCrossGroupOpener(
openerBrowser.browsingContext
);
}
}
} catch (e) {
console.error("Failed to create tab");
console.error(e);
t.remove();
if (t.linkedBrowser) {
this._tabFilters.delete(t);
this._tabListeners.delete(t);
this.getPanel(t.linkedBrowser).remove();
}
return null;
}
// Hack to ensure that the about:newtab, and about:welcome favicon is loaded
// instantaneously, to avoid flickering and improve perceived performance.
this.setDefaultIcon(t, aURIObject);
if (!batchInsertingTabs) {
// Fire a TabOpen event
this._fireTabOpen(t, eventDetail);
if (
!usingPreloadedContent &&
originPrincipal &&
originStoragePrincipal &&
aURI
) {
let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
// Unless we know for sure we're not inheriting principals,
// force the about:blank viewer to have the right principal:
if (
!aURIObject ||
doGetProtocolFlags(aURIObject) & URI_INHERITS_SECURITY_CONTEXT
) {
b.createAboutBlankContentViewer(
originPrincipal,
originStoragePrincipal
);
}
}
// If we didn't swap docShells with a preloaded browser
// then let's just continue loading the page normally.
if (
!usingPreloadedContent &&
(!uriIsAboutBlank || !allowInheritPrincipal) &&
!skipLoad
) {
// pretend the user typed this so it'll be available till
// the document successfully loads
if (aURI && !gInitialPages.includes(aURI)) {
b.userTypedValue = aURI;
}
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (allowThirdPartyFixup) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
}
if (fromExternal) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
} else if (!triggeringPrincipal.isSystemPrincipal) {
// XXX this code must be reviewed and changed when bug 1616353
// lands.
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIRST_LOAD;
}
if (!allowInheritPrincipal) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
}
if (disableTRR) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISABLE_TRR;
}
if (forceAllowDataURI) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
}
try {
b.loadURI(aURI, {
flags,
triggeringPrincipal,
referrerInfo,
charset,
postData,
csp,
globalHistoryOptions,
triggeringRemoteType,
});
} catch (ex) {
console.error(ex);
}
}
}
// This field is updated regardless if we actually animate
// since it's important that we keep this count correct in all cases.
this.tabAnimationsInProgress++;
if (animate) {
requestAnimationFrame(function() {
// kick the animation off
t.setAttribute("fadein", "true");
});
}
// Additionally send pinned tab events
if (pinned) {
this._notifyPinnedStatus(t);
}
gSharedTabWarning.tabAdded(t);
if (!inBackground) {
this.selectedTab = t;
}
return t;
},
addMultipleTabs(restoreTabsLazily, selectTab, aPropertiesTabs) {
let tabs = [];
let tabsFragment = document.createDocumentFragment();
let tabToSelect = null;
let hiddenTabs = new Map();
let shouldUpdateForPinnedTabs = false;
// We create each tab and browser, but only insert them
// into a document fragment so that we can insert them all
// together. This prevents synch reflow for each tab
// insertion.
for (var i = 0; i < aPropertiesTabs.length; i++) {
let tabData = aPropertiesTabs[i];
let userContextId = tabData.userContextId;
let select = i == selectTab - 1;
let tab;
let tabWasReused = false;
// Re-use existing selected tab if possible to avoid the overhead of
// selecting a new tab.
if (
select &&
this.selectedTab.userContextId == userContextId &&
!SessionStore.isTabRestoring(this.selectedTab)
) {
tabWasReused = true;
tab = this.selectedTab;
if (!tabData.pinned) {
this.unpinTab(tab);
} else {
this.pinTab(tab);
}
}
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
restoreTabsLazily && !select && !tabData.pinned;
let url = "about:blank";
if (tabData.entries?.length) {
let activeIndex = (tabData.index || tabData.entries.length) - 1;
// Ensure the index is in bounds.
activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
activeIndex = Math.max(activeIndex, 0);
url = tabData.entries[activeIndex].url;
}
let preferredRemoteType = E10SUtils.getRemoteTypeForURI(
url,
gMultiProcessBrowser,
gFissionBrowser,
E10SUtils.DEFAULT_REMOTE_TYPE,
null,
E10SUtils.predictOriginAttributes({ window, userContextId })
);
// If we're creating a lazy browser, let tabbrowser know the future
// URI because progress listeners won't get onLocationChange
// notification before the browser is inserted.
//
// Setting noInitialLabel is a perf optimization. Rendering tab labels
// would make resizing the tabs more expensive as we're adding them.
// Each tab will get its initial label set in restoreTab.
tab = this.addTrustedTab(createLazyBrowser ? url : "about:blank", {
createLazyBrowser,
skipAnimation: true,
noInitialLabel: true,
userContextId,
skipBackgroundNotify: true,
bulkOrderedOpen: true,
batchInsertingTabs: true,
skipLoad: true,
preferredRemoteType,
});
if (select) {
tabToSelect = tab;
}
}
tabs.push(tab);
if (tabData.pinned) {
// Calling `pinTab` calls `moveTabTo`, which assumes the tab is
// inserted in the DOM. If the tab is not yet in the DOM,
// just insert it in the right place from the start.
if (!tab.parentNode) {
tab._tPos = this._numPinnedTabs;
this.tabContainer.insertBefore(tab, this.tabs[this._numPinnedTabs]);
tab.setAttribute("pinned", "true");
this.tabContainer._invalidateCachedTabs();
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
this._notifyPinnedStatus(tab);
// Once we're done adding all tabs, _updateTabBarForPinnedTabs
// needs calling:
shouldUpdateForPinnedTabs = true;
}
} else {
if (tab.hidden) {
tab.hidden = true;
hiddenTabs.set(tab, tabData.extData && tabData.extData.hiddenBy);
}
tabsFragment.appendChild(tab);
if (tabWasReused) {
this.tabContainer._invalidateCachedTabs();
}
}
tab.initialize();
}
// inject the new DOM nodes
this.tabContainer.appendChild(tabsFragment);
for (let [tab, hiddenBy] of hiddenTabs) {
let event = document.createEvent("Events");
event.initEvent("TabHide", true, false);
tab.dispatchEvent(event);
if (hiddenBy) {
SessionStore.setCustomTabValue(tab, "hiddenBy", hiddenBy);
}
}
this.tabContainer._invalidateCachedTabs();
if (shouldUpdateForPinnedTabs) {
this._updateTabBarForPinnedTabs();
}
// We need to wait until after all tabs have been appended to the DOM
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
this.selectedTab = tabToSelect;
this.removeTab(leftoverTab);
}
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
this.tabContainer._setPositionalAttributes();
TabBarVisibility.update();
for (let tab of tabs) {
// If tabToSelect is a tab, we didn't reuse the selected tab.
if (tabToSelect || !tab.selected) {
// Fire a TabOpen event for all unpinned tabs, except reused selected
// tabs.
if (!tab.pinned) {
this._fireTabOpen(tab, {});
}
// Fire a TabBrowserInserted event on all tabs that have a connected,
// real browser, except for reused selected tabs.
if (tab.linkedPanel) {
var evt = new CustomEvent("TabBrowserInserted", {
bubbles: true,
detail: { insertedOnTabCreation: true },
});
tab.dispatchEvent(evt);
}
}
}
}
return tabs;
},
moveTabsToStart(contextTab) {
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
// Walk the array in reverse order so the tabs are kept in order.
for (let i = tabs.length - 1; i >= 0; i--) {
let tab = tabs[i];
if (tab._tPos > 0) {
this.moveTabTo(tab, 0);
}
}
},
moveTabsToEnd(contextTab) {
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
for (let tab of tabs) {
if (tab._tPos < this.tabs.length - 1) {
this.moveTabTo(tab, this.tabs.length - 1);
}
}
},
warnAboutClosingTabs(tabsToClose, aCloseTabs, aSource) {
if (tabsToClose <= 1) {
return true;
}
const pref =
aCloseTabs == this.closingTabsEnum.ALL
? "browser.tabs.warnOnClose"
: "browser.tabs.warnOnCloseOtherTabs";
var shouldPrompt = Services.prefs.getBoolPref(pref);
if (!shouldPrompt) {
return true;
}
const maxTabsUndo = Services.prefs.getIntPref(
"browser.sessionstore.max_tabs_undo"
);
if (
aCloseTabs != this.closingTabsEnum.ALL &&
tabsToClose <= maxTabsUndo
) {
return true;
}
// Our prompt to close this window is most important, so replace others.
gDialogBox.replaceDialogIfOpen();
var ps = Services.prompt;
// default to true: if it were false, we wouldn't get this far
var warnOnClose = { value: true };
// focus the window before prompting.
// this will raise any minimized window, which will
// make it obvious which window the prompt is for and will
// solve the problem of windows "obscuring" the prompt.
// see bug #350299 for more details
window.focus();
const [title, button, checkbox] = this.tabLocalization.formatValuesSync([
{
id: "tabbrowser-confirm-close-tabs-title",
args: { tabCount: tabsToClose },
},
{ id: "tabbrowser-confirm-close-tabs-button" },
{ id: "tabbrowser-confirm-close-tabs-checkbox" },
]);
let flags =
ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 +
ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1;
let checkboxLabel =
aCloseTabs == this.closingTabsEnum.ALL ? checkbox : null;
var buttonPressed = ps.confirmEx(
window,
title,
null,
flags,
button,
null,
null,
checkboxLabel,
warnOnClose
);
Services.telemetry.setEventRecordingEnabled("close_tab_warning", true);
let closeTabEnumKey =
Object.entries(this.closingTabsEnum)
.find(([k, v]) => v == aCloseTabs)?.[0]
?.toLowerCase() || "some";
let warnCheckbox = warnOnClose.value ? "checked" : "unchecked";
if (!checkboxLabel) {
warnCheckbox = "not-present";
}
let sessionWillBeRestored =
Services.prefs.getIntPref("browser.startup.page") == 3 ||
Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
let closesWindow = aCloseTabs == this.closingTabsEnum.ALL;
Services.telemetry.recordEvent(
"close_tab_warning",
"shown",
closesWindow ? "window" : "tabs",
null,
{
source: aSource || `close-${closeTabEnumKey}-tabs`,
button: buttonPressed == 0 ? "close" : "cancel",
warn_checkbox: warnCheckbox,
closing_tabs: "" + tabsToClose,
closing_wins: "" + +closesWindow, // ("1" or "0", depending on the value)
// This value doesn't really apply to whether this warning
// gets shown, but having pings be consistent (and perhaps
// being able to see trends for users with/without sessionrestore)
// seems useful:
will_restore: sessionWillBeRestored ? "yes" : "no",
}
);
var reallyClose = buttonPressed == 0;
// don't set the pref unless they press OK and it's false
if (
aCloseTabs == this.closingTabsEnum.ALL &&
reallyClose &&
!warnOnClose.value
) {
Services.prefs.setBoolPref(pref, false);
}
return reallyClose;
},
/**
* This determines where the tab should be inserted within the tabContainer
*/
_insertTabAtIndex(
tab,
{ index, ownerTab, openerTab, pinned, bulkOrderedOpen } = {}
) {
// If this new tab is owned by another, assert that relationship
if (ownerTab) {
tab.owner = ownerTab;
}
// Ensure we have an index if one was not provided.
if (typeof index != "number") {
// Move the new tab after another tab if needed, to the end otherwise.
index = Infinity;
if (
!bulkOrderedOpen &&
((openerTab &&
Services.prefs.getBoolPref(
"browser.tabs.insertRelatedAfterCurrent"
)) ||
Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent"))
) {
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
if (previousTab.multiselected) {
previousTab = this.selectedTabs.at(-1);
}
if (!previousTab.hidden) {
index = previousTab._tPos + 1;
} else if (previousTab == FirefoxViewHandler.tab) {
index = 0;
}
if (lastRelatedTab) {
lastRelatedTab.owner = null;
} else if (openerTab) {
tab.owner = openerTab;
}
// Always set related map if opener exists.
if (openerTab) {
this._lastRelatedTabMap.set(openerTab, tab);
}
}
}
// Ensure index is within bounds.
if (pinned) {
index = Math.max(index, 0);
index = Math.min(index, this._numPinnedTabs);
} else {
index = Math.max(index, this._numPinnedTabs);
index = Math.min(index, this.tabs.length);
}
let tabAfter = this.tabs[index] || null;
this.tabContainer._invalidateCachedTabs();
// Prevent a flash of unstyled content by setting up the tab content
// and inherited attributes before appending it (see Bug 1592054):
tab.initialize();
this.tabContainer.insertBefore(tab, tabAfter);
if (tabAfter) {
this._updateTabsAfterInsert();
} else {
tab._tPos = index;
}
if (pinned) {
this._updateTabBarForPinnedTabs();
}
this.tabContainer._setPositionalAttributes();
TabBarVisibility.update();
},
/**
* Dispatch a new tab event. This should be called when things are in a
* consistent state, such that listeners of this event can again open
* or close tabs.
*/
_fireTabOpen(tab, eventDetail) {
delete tab.initializingTab;
let evt = new CustomEvent("TabOpen", {
bubbles: true,
detail: eventDetail || {},
});
tab.dispatchEvent(evt);
},
getTabsToTheStartFrom(aTab) {
let tabsToStart = [];
if (aTab.hidden) {
return tabsToStart;
}
let tabs = this.visibleTabs;
for (let i = 0; i < tabs.length; ++i) {
if (tabs[i] == aTab) {
break;
}
// Ignore pinned tabs.
if (tabs[i].pinned) {
continue;
}
// In a multi-select context, select all unselected tabs
// starting from the context tab.
if (aTab.multiselected && tabs[i].multiselected) {
continue;
}
tabsToStart.push(tabs[i]);
}
return tabsToStart;
},
getTabsToTheEndFrom(aTab) {
let tabsToEnd = [];
if (aTab.hidden) {
return tabsToEnd;
}
let tabs = this.visibleTabs;
for (let i = tabs.length - 1; i >= 0; --i) {
if (tabs[i] == aTab) {
break;
}
// Ignore pinned tabs.
if (tabs[i].pinned) {
continue;
}
// In a multi-select context, select all unselected tabs
// starting from the context tab.
if (aTab.multiselected && tabs[i].multiselected) {
continue;
}
tabsToEnd.push(tabs[i]);
}
return tabsToEnd;
},
/**
* In a multi-select context, the tabs (except pinned tabs) that are located to the
* left of the leftmost selected tab will be removed.
*/
removeTabsToTheStartFrom(aTab) {
let tabs = this.getTabsToTheStartFrom(aTab);
if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
) {
return;
}
this.removeTabs(tabs);
},
/**
* In a multi-select context, the tabs (except pinned tabs) that are located to the
* right of the rightmost selected tab will be removed.
*/
removeTabsToTheEndFrom(aTab) {
let tabs = this.getTabsToTheEndFrom(aTab);
if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
) {
return;
}
this.removeTabs(tabs);
},
/**
* In a multi-select context, all unpinned and unselected tabs are removed.
* Otherwise all unpinned tabs except aTab are removed.
*
* @param aTab
* The tab we will skip removing
* @param aParams
* An optional set of parameters that will be passed to the
* removeTabs function.
*/
removeAllTabsBut(aTab, aParams) {
let tabsToRemove = [];
if (aTab && aTab.multiselected) {
tabsToRemove = this.visibleTabs.filter(
tab => !tab.multiselected && !tab.pinned
);
} else {
tabsToRemove = this.visibleTabs.filter(
tab => tab != aTab && !tab.pinned
);
}
if (
!this.warnAboutClosingTabs(
tabsToRemove.length,
this.closingTabsEnum.OTHER
)
) {
return;
}
this.removeTabs(tabsToRemove, aParams);
},
removeMultiSelectedTabs() {
let selectedTabs = this.selectedTabs;
if (
!this.warnAboutClosingTabs(
selectedTabs.length,
this.closingTabsEnum.MULTI_SELECTED
)
) {
return;
}
this.removeTabs(selectedTabs);
},
/**
* @typedef {object} _startRemoveTabsReturnValue
* @property {Promise} beforeUnloadComplete
* A promise that is resolved once all the beforeunload handlers have been
* called.
* @property {object[]} tabsWithBeforeUnloadPrompt
* An array of tabs with unload prompts that need to be handled.
* @property {object} [lastToClose]
* The last tab to be closed, if appropriate.
*/
/**
* Starts to remove tabs from the UI: checking for beforeunload handlers,
* closing tabs where possible and triggering running of the unload handlers.
*
* @param {object[]} tabs
* The set of tabs to remove.
* @param {object} options
* @param {boolean} options.animate
* Whether or not to animate closing.
* @param {boolean} options.suppressWarnAboutClosingWindow
* This will supress the warning about closing a window with the last tab.
* @param {boolean} options.skipPermitUnload
* Skips the before unload checks for the tabs. Only set this to true when
* using it in tandem with `runBeforeUnloadForTabs`.
* @param {boolean} options.skipRemoves
* Skips actually removing the tabs. The beforeunload handlers still run.
* @returns {_startRemoveTabsReturnValue}
*/
_startRemoveTabs(
tabs,
{ animate, suppressWarnAboutClosingWindow, skipPermitUnload, skipRemoves }
) {
// Note: if you change any of the unload algorithm, consider also
// changing `runBeforeUnloadForTabs` above.
let tabsWithBeforeUnloadPrompt = [];
let tabsWithoutBeforeUnload = [];
let beforeUnloadPromises = [];
let lastToClose;
for (let tab of tabs) {
if (!skipRemoves) {
tab._closedInGroup = true;
}
if (!skipRemoves && tab.selected) {
lastToClose = tab;
let toBlurTo = this._findTabToBlurTo(lastToClose, tabs);
if (toBlurTo) {
this._getSwitcher().warmupTab(toBlurTo);
}
} else if (!skipPermitUnload && this._hasBeforeUnload(tab)) {
TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", tab);
// We need to block while calling permitUnload() because it
// processes the event queue and may lead to another removeTab()
// call before permitUnload() returns.
tab._pendingPermitUnload = true;
beforeUnloadPromises.push(
// To save time, we first run the beforeunload event listeners in all
// content processes in parallel. Tabs that would have shown a prompt
// will be handled again later.
tab.linkedBrowser.asyncPermitUnload("dontUnload").then(
({ permitUnload }) => {
tab._pendingPermitUnload = false;
TelemetryStopwatch.finish(
"FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS",
tab
);
if (tab.closing) {
// The tab was closed by the user while we were in permitUnload, don't
// attempt to close it a second time.
} else if (permitUnload) {
if (!skipRemoves) {
// OK to close without prompting, do it immediately.
this.removeTab(tab, {
animate,
prewarmed: true,
skipPermitUnload: true,
});
}
} else {
// We will need to prompt, queue it so it happens sequentially.
tabsWithBeforeUnloadPrompt.push(tab);
}
},
err => {
console.error("error while calling asyncPermitUnload", err);
tab._pendingPermitUnload = false;
TelemetryStopwatch.finish(
"FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS",
tab
);
}
)
);
} else {
tabsWithoutBeforeUnload.push(tab);
}
}
// Now that all the beforeunload IPCs have been sent to content processes,
// we can queue unload messages for all the tabs without beforeunload listeners.
// Doing this first would cause content process main threads to be busy and delay
// beforeunload responses, which would be user-visible.
if (!skipRemoves) {
for (let tab of tabsWithoutBeforeUnload) {
this.removeTab(tab, {
animate,
prewarmed: true,
skipPermitUnload,
});
}
}
return {
beforeUnloadComplete: Promise.all(beforeUnloadPromises),
tabsWithBeforeUnloadPrompt,
lastToClose,
};
},
/**
* Runs the before unload handler for the provided tabs, waiting for them
* to complete.
*
* This can be used in tandem with removeTabs to allow any before unload
* prompts to happen before any tab closures. This should only be used
* in the case where any prompts need to happen before other items before
* the actual tabs are closed.
*
* When using this function alongside removeTabs, specify the `skipUnload`
* option to removeTabs.
*
* @param {object[]} tabs
* An array of tabs to remove.
* @returns {Promise<boolean>}
* Returns true if the unload has been blocked by the user. False if tabs
* may be subsequently closed.
*/
async runBeforeUnloadForTabs(tabs) {
try {
let {
beforeUnloadComplete,
tabsWithBeforeUnloadPrompt,
} = this._startRemoveTabs(tabs, {
animate: false,
suppressWarnAboutClosingWindow: false,
skipPermitUnload: false,
skipRemoves: true,
});
await beforeUnloadComplete;
// Now run again sequentially the beforeunload listeners that will result in a prompt.
for (let tab of tabsWithBeforeUnloadPrompt) {
tab._pendingPermitUnload = true;
let { permitUnload } = this.getBrowserForTab(tab).permitUnload();
tab._pendingPermitUnload = false;
if (!permitUnload) {
return true;
}
}
} catch (e) {
console.error(e);
}
return false;
},
/**
* Removes multiple tabs from the tab browser.
*
* @param {object[]} tabs
* The set of tabs to remove.
* @param {object} [options]
* @param {boolean} [options.animate]
* Whether or not to animate closing, defaults to true.
* @param {boolean} [options.suppressWarnAboutClosingWindow]
* This will supress the warning about closing a window with the last tab.
* @param {boolean} [options.skipPermitUnload]
* Skips the before unload checks for the tabs. Only set this to true when
* using it in tandem with `runBeforeUnloadForTabs`.
*/
removeTabs(
tabs,
{
animate = true,
suppressWarnAboutClosingWindow = false,
skipPermitUnload = false,
} = {}
) {
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
this.tabs.length == tabs.length &&
Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab")
) {
window.closeWindow(
true,
suppressWarnAboutClosingWindow ? null : window.warnAboutClosingWindow,
"close-last-tab"
);
return;
}
SessionStore.resetLastClosedTabCount(window);
this._clearMultiSelectionLocked = true;
// Guarantee that _clearMultiSelectionLocked lock gets released.
try {
let {
beforeUnloadComplete,
tabsWithBeforeUnloadPrompt,
lastToClose,
} = this._startRemoveTabs(tabs, {
animate,
suppressWarnAboutClosingWindow,
skipPermitUnload,
skipRemoves: false,
});
// Wait for all the beforeunload events to have been processed by content processes.
// The permitUnload() promise will, alas, not call its resolution
// callbacks after the browser window the promise lives in has closed,
// so we have to check for that case explicitly.
let done = false;
beforeUnloadComplete.then(() => {
done = true;
});
Services.tm.spinEventLoopUntilOrQuit(
"tabbrowser.js:removeTabs",
() => done || window.closed
);
if (!done) {
return;
}
let aParams = {
animate,
prewarmed: true,
skipPermitUnload,
};
// Now run again sequentially the beforeunload listeners that will result in a prompt.
for (let tab of tabsWithBeforeUnloadPrompt) {
this.removeTab(tab, aParams);
if (!tab.closing) {
// If we abort the closing of the tab.
tab._closedInGroup = false;
}
}
// Avoid changing the selected browser several times by removing it,
// if appropriate, lastly.
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
} catch (e) {
console.error(e);
}
this._clearMultiSelectionLocked = false;
this._avoidSingleSelectedTab();
// Don't use document.l10n.setAttributes because the FTL file is loaded
// lazily and we won't be able to resolve the string.
document.getElementById("History:UndoCloseTab").setAttribute(
"data-l10n-args",
JSON.stringify({
tabCount: SessionStore.getLastClosedTabCount(window),
})
);
},
removeCurrentTab(aParams) {
this.removeTab(this.selectedTab, aParams);
},
removeTab(
aTab,
{
animate,
byMouse,
skipPermitUnload,
closeWindowWithLastTab,
prewarmed,
} = {}
) {
if (UserInteraction.running("browser.tabs.opening", window)) {
UserInteraction.finish("browser.tabs.opening", window);
}
// Telemetry stopwatches may already be running if removeTab gets
// called again for an already closing tab.
if (
!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_ANIM_MS", aTab) &&
!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab)
) {
// Speculatevely start both stopwatches now. We'll cancel one of
// the two later depending on whether we're animating.
TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
}
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
this._endRemoveTab(aTab);
return;
}
let isLastTab = !aTab.hidden && this.visibleTabs.length == 1;
let windowUtils = window.windowUtils;
// We have to sample the tab width now, since _beginRemoveTab might
// end up modifying the DOM in such a way that aTab gets a new
// frame created for it (for example, by updating the visually selected
// state).
let tabWidth = windowUtils.getBoundsWithoutFlushing(aTab).width;
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
skipPermitUnload,
closeWindowWithLastTab,
prewarmed,
})
) {
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
return;
}
if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse) {
this.tabContainer._lockTabSizing(aTab, tabWidth);
} else {
this.tabContainer._unlockTabSizing();
}
if (
!animate /* the caller didn't opt in */ ||
gReduceMotion ||
isLastTab ||
aTab.pinned ||
aTab.hidden ||
this._removingTabs.size >
3 /* don't want lots of concurrent animations */ ||
aTab.getAttribute("fadein") !=
"true" /* fade-in transition hasn't been triggered yet */ ||
window.getComputedStyle(aTab).maxWidth ==
"0.1px" /* fade-in transition hasn't moved yet */
) {
// We're not animating, so we can cancel the animation stopwatch.
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
this._endRemoveTab(aTab);
return;
}
// We're animating, so we can cancel the non-animation stopwatch.
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
aTab.style.maxWidth = ""; // ensure that fade-out transition happens
aTab.removeAttribute("fadein");
aTab.removeAttribute("bursting");
setTimeout(
function(tab, tabbrowser) {
if (
tab.container &&
window.getComputedStyle(tab).maxWidth == "0.1px"
) {
console.assert(
false,
"Giving up waiting for the tab closing animation to finish (bug 608589)"
);
tabbrowser._endRemoveTab(tab);
}
},
3000,
aTab,
this
);
},
_hasBeforeUnload(aTab) {
let browser = aTab.linkedBrowser;
if (browser.isRemoteBrowser && browser.frameLoader) {
return browser.hasBeforeUnload;
}
return false;
},
_beginRemoveTab(
aTab,
{
adoptedByTab,
closeWindowWithLastTab,
closeWindowFastpath,
skipPermitUnload,
prewarmed,
} = {}
) {
if (aTab.closing || this._windowIsClosing) {
return false;
}
var browser = this.getBrowserForTab(aTab);
if (
!skipPermitUnload &&
!adoptedByTab &&
aTab.linkedPanel &&
!aTab._pendingPermitUnload &&
(!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))
) {
if (!prewarmed) {
let blurTab = this._findTabToBlurTo(aTab);
if (blurTab) {
this.warmupTab(blurTab);
}
}
TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
// We need to block while calling permitUnload() because it
// processes the event queue and may lead to another removeTab()
// call before permitUnload() returns.
aTab._pendingPermitUnload = true;
let { permitUnload } = browser.permitUnload();
aTab._pendingPermitUnload = false;
TelemetryStopwatch.finish("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
// If we were closed during onbeforeunload, we return false now
// so we don't (try to) close the same tab again. Of course, we
// also stop if the unload was cancelled by the user:
if (aTab.closing || !permitUnload) {
return false;
}
}
// this._switcher would normally cover removing a tab from this
// cache, but we may not have one at this time.
let tabCacheIndex = this._tabLayerCache.indexOf(aTab);
if (tabCacheIndex != -1) {
this._tabLayerCache.splice(tabCacheIndex, 1);
}
// Delay hiding the the active tab if we're screen sharing.
// See Bug 1642747.
let screenShareInActiveTab =
aTab == this.selectedTab && aTab._sharingState?.webRTC?.screen;
if (!screenShareInActiveTab) {
this._blurTab(aTab);
}
var closeWindow = false;
var newTab = false;
if (!aTab.hidden && this.visibleTabs.length == 1) {
closeWindow =
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
// so avoid calling it again:
window.skipNextCanClose = true;
}
// Closing the tab and replacing it with a blank one is notably slower
// than closing the window right away. If the caller opts in, take
// the fast path.
if (closeWindow && closeWindowFastpath && !this._removingTabs.size) {
// This call actually closes the window, unless the user
// cancels the operation. We are finished here in both cases.
this._windowIsClosing = window.closeWindow(
true,
window.warnAboutClosingWindow,
"close-last-tab"
);
return false;
}
newTab = true;
}
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
if (closeWindow && adoptedByTab) {
// Remove the tab's filter and progress listener to avoid leaking.
if (aTab.linkedPanel) {
const filter = this._tabFilters.get(aTab);
browser.webProgress.removeProgressListener(filter);
const listener = this._tabListeners.get(aTab);
filter.removeProgressListener(listener);
listener.destroy();
this._tabListeners.delete(aTab);
this._tabFilters.delete(aTab);
}
return true;
}
if (!aTab._fullyOpen) {
// If the opening tab animation hasn't finished before we start closing the
// tab, decrement the animation count since _handleNewTab will not get called.
this.tabAnimationsInProgress--;
}
this.tabAnimationsInProgress++;
// Mute audio immediately to improve perceived speed of tab closure.
if (!adoptedByTab && aTab.hasAttribute("soundplaying")) {
// Don't persist the muted state as this wasn't a user action.
// This lets undo-close-tab return it to an unmuted state.
aTab.linkedBrowser.mute(true);
}
aTab.closing = true;
this._removingTabs.add(aTab);
this.tabContainer._invalidateCachedTabs();
// Invalidate hovered tab state tracking for this closing tab.
aTab._mouseleave();
if (newTab) {
this.addTrustedTab(BROWSER_NEW_TAB_URL, {
skipAnimation: true,
});
} else {
TabBarVisibility.update();
}
// Splice this tab out of any lines of succession before any events are
// dispatched.
this.replaceInSuccession(aTab, aTab.successor);
this.setSuccessor(aTab, null);
// We're committed to closing the tab now.
// Dispatch a notification.
// We dispatch it before any teardown so that event listeners can
// inspect the tab that's about to close.
let evt = new CustomEvent("TabClose", {
bubbles: true,
detail: { adoptedBy: adoptedByTab },
});
aTab.dispatchEvent(evt);
if (this.tabs.length == 2) {
// We're closing one of our two open tabs, inform the other tab that its
// sibling is going away.
this.tabs[0].linkedBrowser.sendMessageToActor(
"Browser:HasSiblings",
false,
"BrowserTab"
);
this.tabs[1].linkedBrowser.sendMessageToActor(
"Browser:HasSiblings",
false,
"BrowserTab"
);
}
let notificationBox = this.readNotificationBox(browser);
notificationBox?._stack?.remove();
if (aTab.linkedPanel) {
if (!adoptedByTab && !gMultiProcessBrowser) {
// Prevent this tab from showing further dialogs, since we're closing it
browser.contentWindow.windowUtils.disableDialogs();
}
// Remove the tab's filter and progress listener.
const filter = this._tabFilters.get(aTab);
browser.webProgress.removeProgressListener(filter);
const listener = this._tabListeners.get(aTab);
filter.removeProgressListener(listener);
listener.destroy();
}
if (browser.registeredOpenURI && !adoptedByTab) {
let userContextId = browser.getAttribute("usercontextid") || 0;
this.UrlbarProviderOpenTabs.unregisterOpenTab(
browser.registeredOpenURI.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
);
delete browser.registeredOpenURI;
}
// We are no longer the primary content area.
browser.removeAttribute("primary");
// Remove this tab as the owner of any other tabs, since it's going away.
for (let tab of this.tabs) {
if ("owner" in tab && tab.owner == aTab) {
// |tab| is a child of the tab we're removing, make it an orphan
tab.owner = null;
}
}
return true;
},
_endRemoveTab(aTab) {
if (!aTab || !aTab._endRemoveArgs) {
return;
}
var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
aTab._endRemoveArgs = null;
if (this._windowIsClosing) {
aCloseWindow = false;
aNewTab = false;
}
this.tabAnimationsInProgress--;
this._lastRelatedTabMap = new WeakMap();
// update the UI early for responsiveness
aTab.collapsed = true;
this._blurTab(aTab);
this._removingTabs.delete(aTab);
if (aCloseWindow) {
this._windowIsClosing = true;
for (let tab of this._removingTabs) {
this._endRemoveTab(tab);
}
} else if (!this._windowIsClosing) {
if (aNewTab) {
gURLBar.select();
}
// workaround for bug 345399
this.tabContainer.arrowScrollbox._updateScrollButtonsDisabledState();
}
// We're going to remove the tab and the browser now.
this._tabFilters.delete(aTab);
this._tabListeners.delete(aTab);
var browser = this.getBrowserForTab(aTab);
if (aTab.linkedPanel) {
// Because of the fact that we are setting JS properties on
// the browser elements, and we have code in place
// to preserve the JS objects for any elements that have
// JS properties set on them, the browser element won't be
// destroyed until the document goes away. So we force a
// cleanup ourselves.
// This has to happen before we remove the child since functions
// like `getBrowserContainer` expect the browser to be parented.
browser.destroy();
}
var wasPinned = aTab.pinned;
// Remove the tab ...
aTab.remove();
this.tabContainer._invalidateCachedTabs();
// Update hashiddentabs if this tab was hidden.
if (aTab.hidden) {
this.tabContainer._updateHiddenTabsStatus();
}
// ... and fix up the _tPos properties immediately.
for (let i = aTab._tPos; i < this.tabs.length; i++) {
this.tabs[i]._tPos = i;
}
if (!this._windowIsClosing) {
if (wasPinned) {
this.tabContainer._positionPinnedTabs();
}
// update tab close buttons state
this.tabContainer._updateCloseButtons();
setTimeout(
function(tabs) {
tabs._lastTabClosedByMouse = false;
},
0,
this.tabContainer
);
}
// update tab positional properties and attributes
this.selectedTab._selected = true;
this.tabContainer._setPositionalAttributes();
// Removing the panel requires fixing up selectedPanel immediately
// (see below), which would be hindered by the potentially expensive
// browser removal. So we remove the browser and the panel in two
// steps.
var panel = this.getPanel(browser);
// In the multi-process case, it's possible an asynchronous tab switch
// is still underway. If so, then it's possible that the last visible
// browser is the one we're in the process of removing. There's the
// risk of displaying preloaded browsers that are at the end of the
// deck if we remove the browser before the switch is complete, so
// we alert the switcher in order to show a spinner instead.
if (this._switcher) {
this._switcher.onTabRemoved(aTab);
}
// This will unload the document. An unload handler could remove
// dependant tabs, so it's important that the tabbrowser is now in
// a consistent state (tab removed, tab positions updated, etc.).
browser.remove();
// Release the browser in case something is erroneously holding a
// reference to the tab after its removal.
this._tabForBrowser.delete(aTab.linkedBrowser);
aTab.linkedBrowser = null;
panel.remove();
// closeWindow might wait an arbitrary length of time if we're supposed
// to warn about closing the window, so we'll just stop the tab close
// stopwatches here instead.
TelemetryStopwatch.finish(
"FX_TAB_CLOSE_TIME_ANIM_MS",
aTab,
true /* aCanceledOkay */
);
TelemetryStopwatch.finish(
"FX_TAB_CLOSE_TIME_NO_ANIM_MS",
aTab,
true /* aCanceledOkay */
);
if (aCloseWindow) {
this._windowIsClosing = closeWindow(
true,
window.warnAboutClosingWindow,
"close-last-tab"
);
}
},
/**
* Finds the tab that we will blur to if we blur aTab.
* @param aTab
* The tab we would blur
* @param aExcludeTabs
* Tabs to exclude from our search (i.e., because they are being
* closed along with aTab)
*/
_findTabToBlurTo(aTab, aExcludeTabs = []) {
if (!aTab.selected) {
return null;
}
if (FirefoxViewHandler.tab) {
aExcludeTabs.push(FirefoxViewHandler.tab);
}
let excludeTabs = new Set(aExcludeTabs);
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
if (aTab.successor && !excludeTabs.has(aTab.successor)) {
return aTab.successor;
}
if (
aTab.owner &&
!aTab.owner.hidden &&
!aTab.owner.closing &&
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
return aTab.owner;
}
// Switch to a visible tab unless there aren't any others remaining
let remainingTabs = this.visibleTabs;
let numTabs = remainingTabs.length;
if (numTabs == 0 || (numTabs == 1 && remainingTabs[0] == aTab)) {
remainingTabs = Array.prototype.filter.call(
this.tabs,
tab => !tab.closing && !excludeTabs.has(tab)
);
}
// Try to find a remaining tab that comes after the given tab
let tab = this.tabContainer.findNextTab(aTab, {
direction: 1,
filter: _tab => remainingTabs.includes(_tab),
});
if (!tab) {
tab = this.tabContainer.findNextTab(aTab, {
direction: -1,
filter: _tab => remainingTabs.includes(_tab),
});
}
return tab;
},
_blurTab(aTab) {
this.selectedTab = this._findTabToBlurTo(aTab);
},
/**
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
PrivateBrowsingUtils.isWindowPrivate(window) !=
PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal)
) {
return false;
}
// Do not allow transfering a useRemoteSubframes tab to a
// non-useRemoteSubframes window and vice versa.
if (gFissionBrowser != aOtherTab.ownerGlobal.gFissionBrowser) {
return false;
}
let ourBrowser = this.getBrowserForTab(aOurTab);
let otherBrowser = aOtherTab.linkedBrowser;
// Can't swap between chrome and content processes.
if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) {
return false;
}
// Keep the userContextId if set on other browser
if (otherBrowser.hasAttribute("usercontextid")) {
ourBrowser.setAttribute(
"usercontextid",
otherBrowser.getAttribute("usercontextid")
);
}
// That's gBrowser for the other window, not the tab's browser!
var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
var isPending = aOtherTab.hasAttribute("pending");
let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
let stateFlags = 0;
if (otherTabListener) {
stateFlags = otherTabListener.mStateFlags;
}
// Expedite the removal of the icon if it was already scheduled.
if (aOtherTab._soundPlayingAttrRemovalTimer) {
clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
aOtherTab._soundPlayingAttrRemovalTimer = 0;
aOtherTab.removeAttribute("soundplaying");
remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
}
// First, start teardown of the other browser. Make sure to not
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
})
) {
return false;
}
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
let [closeWindow] = aOtherTab._endRemoveArgs;
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
// Only suppressing window animations isn't enough to avoid
// an empty content area being painted.
let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
baseWin.visibility = false;
}
let modifiedAttrs = [];
if (aOtherTab.hasAttribute("muted")) {
aOurTab.setAttribute("muted", "true");
aOurTab.muteReason = aOtherTab.muteReason;
// For non-lazy tabs, mute() must be called.
if (aOurTab.linkedPanel) {
ourBrowser.mute();
}
modifiedAttrs.push("muted");
}
if (aOtherTab.hasAttribute("soundplaying")) {
aOurTab.setAttribute("soundplaying", "true");
modifiedAttrs.push("soundplaying");
}
if (aOtherTab.hasAttribute("usercontextid")) {
aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
modifiedAttrs.push("usercontextid");
}
if (aOtherTab.hasAttribute("sharing")) {
aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
modifiedAttrs.push("sharing");
aOurTab._sharingState = aOtherTab._sharingState;
webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
}
if (aOtherTab.hasAttribute("pictureinpicture")) {
aOurTab.setAttribute("pictureinpicture", true);
modifiedAttrs.push("pictureinpicture");
let event = new CustomEvent("TabSwapPictureInPicture", {
detail: aOurTab,
});
aOtherTab.dispatchEvent(event);
}
SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
// If the other tab is pending (i.e. has not been restored, yet)
// then do not switch docShells but retrieve the other tab's state
// and apply it to our tab.
if (isPending) {
// Tag tab so that the extension framework can ignore tab events that
// are triggered amidst the tab/browser restoration process
// (TabHide, TabPinned, TabUnpinned, "muted" attribute changes, etc.).
aOurTab.initializingTab = true;
delete ourBrowser._cachedCurrentURI;
SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
delete aOurTab.initializingTab;
// Make sure to unregister any open URIs.
this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
} else {
// Workarounds for bug 458697
// Icon might have been set on DOMLinkAdded, don't override that.
if (!ourBrowser.mIconURL && otherBrowser.mIconURL) {
this.setIcon(aOurTab, otherBrowser.mIconURL);
}
var isBusy = aOtherTab.hasAttribute("busy");
if (isBusy) {
aOurTab.setAttribute("busy", "true");
modifiedAttrs.push("busy");
if (aOurTab.selected) {
this._isBusy = true;
}
}
this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
}
// Unregister the previously opened URI
if (otherBrowser.registeredOpenURI) {
let userContextId = otherBrowser.getAttribute("usercontextid") || 0;
this.UrlbarProviderOpenTabs.unregisterOpenTab(
otherBrowser.registeredOpenURI.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
);
delete otherBrowser.registeredOpenURI;
}
// Handle findbar data (if any)
let otherFindBar = aOtherTab._findBar;
if (otherFindBar && otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
let oldValue = otherFindBar._findField.value;
let wasHidden = otherFindBar.hidden;
let ourFindBarPromise = this.getFindBar(aOurTab);
ourFindBarPromise.then(ourFindBar => {
if (!ourFindBar) {
return;
}
ourFindBar._findField.value = oldValue;
if (!wasHidden) {
ourFindBar.onFindCommand();
}
});
}
// Finish tearing down the tab that's going away.
if (closeWindow) {
aOtherTab.ownerGlobal.close();
} else {
remoteBrowser._endRemoveTab(aOtherTab);
}
this.setTabTitle(aOurTab);
// If the tab was already selected (this happens in the scenario
// of replaceTabWithWindow), notify onLocationChange, etc.
if (aOurTab.selected) {
this.updateCurrentBrowser(true);
}
if (modifiedAttrs.length) {
this._tabAttrModified(aOurTab, modifiedAttrs);
}
return true;
},
swapBrowsers(aOurTab, aOtherTab) {
let otherBrowser = aOtherTab.linkedBrowser;
let otherTabBrowser = otherBrowser.getTabBrowser();
// We aren't closing the other tab so, we also need to swap its tablisteners.
let filter = otherTabBrowser._tabFilters.get(aOtherTab);
let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
otherBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(tabListener);
// Perform the docshell swap through the common mechanism.
this._swapBrowserDocShells(aOurTab, otherBrowser);
// Restore the listeners for the swapped in tab.
tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener(
aOtherTab,
otherBrowser,
false,
false
);
otherTabBrowser._tabListeners.set(aOtherTab, tabListener);
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
filter.addProgressListener(tabListener, notifyAll);
otherBrowser.webProgress.addProgressListener(filter, notifyAll);
},
_swapBrowserDocShells(aOurTab, aOtherBrowser, aStateFlags) {
// aOurTab's browser needs to be inserted now if it hasn't already.
this._insertBrowser(aOurTab);
// Unhook our progress listener
const filter = this._tabFilters.get(aOurTab);
let tabListener = this._tabListeners.get(aOurTab);
let ourBrowser = this.getBrowserForTab(aOurTab);
ourBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(tabListener);
// Make sure to unregister any open URIs.
this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser;
// If switcher is active, it will intercept swap events and
// react as needed.
if (!this._switcher) {
aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(
ourBrowser
);
}
// Swap the docshells
ourBrowser.swapDocShells(aOtherBrowser);
// Swap permanentKey properties.
let ourPermanentKey = ourBrowser.permanentKey;
ourBrowser.permanentKey = aOtherBrowser.permanentKey;
aOtherBrowser.permanentKey = ourPermanentKey;
aOurTab.permanentKey = ourBrowser.permanentKey;
if (remoteBrowser) {
let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
if (otherTab) {
otherTab.permanentKey = aOtherBrowser.permanentKey;
}
}
// Restore the progress listener
tabListener = new TabProgressListener(
aOurTab,
ourBrowser,
false,
false,
aStateFlags
);
this._tabListeners.set(aOurTab, tabListener);
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
filter.addProgressListener(tabListener, notifyAll);
ourBrowser.webProgress.addProgressListener(filter, notifyAll);
},
_swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
// Swap the registeredOpenURI properties of the two browsers
let tmp = aOurBrowser.registeredOpenURI;
delete aOurBrowser.registeredOpenURI;
if (aOtherBrowser.registeredOpenURI) {
aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
delete aOtherBrowser.registeredOpenURI;
}
if (tmp) {
aOtherBrowser.registeredOpenURI = tmp;
}
},
announceWindowCreated(browser, userContextId) {
let tab = this.getTabForBrowser(browser);
if (tab) {
if (userContextId) {
ContextualIdentityService.telemetry(userContextId);
tab.setUserContextId(userContextId);
}
browser.sendMessageToActor(
"Browser:AppTab",
{ isAppTab: tab.pinned },
"BrowserTab"
);
}
// We don't want to update the container icon and identifier if
// this is not the selected browser.
if (browser == gBrowser.selectedBrowser) {
updateUserContextUIIndicator();
}
},
reloadMultiSelectedTabs() {
this.reloadTabs(this.selectedTabs);
},
reloadTabs(tabs) {
for (let tab of tabs) {
try {
this.getBrowserForTab(tab).reload();
} catch (e) {
// ignore failure to reload so others will be reloaded
}
}
},
reloadTab(aTab) {
let browser = this.getBrowserForTab(aTab);
// Reset temporary permissions on the current tab. This is done here
// because we only want to reset permissions on user reload.
SitePermissions.clearTemporaryBlockPermissions(browser);
// Also reset DOS mitigations for the basic auth prompt on reload.
delete browser.authPromptAbuseCounter;
gIdentityHandler.hidePopup();
gPermissionPanel.hidePopup();
browser.reload();
},
addProgressListener(aListener) {
if (arguments.length != 1) {
console.error(
"gBrowser.addProgressListener was " +
"called with a second argument, " +
"which is not supported. See bug " +
"608628. Call stack: ",
new Error().stack
);
}
this.mProgressListeners.push(aListener);
},
removeProgressListener(aListener) {
this.mProgressListeners = this.mProgressListeners.filter(
l => l != aListener
);
},
addTabsProgressListener(aListener) {
this.mTabsProgressListeners.push(aListener);
},
removeTabsProgressListener(aListener) {
this.mTabsProgressListeners = this.mTabsProgressListeners.filter(
l => l != aListener
);
},
getBrowserForTab(aTab) {
return aTab.linkedBrowser;
},
showOnlyTheseTabs(aTabs) {
for (let tab of this.tabs) {
if (!aTabs.includes(tab)) {
this.hideTab(tab);
} else {
this.showTab(tab);
}
}
this.tabContainer._updateHiddenTabsStatus();
this.tabContainer._handleTabSelect(true);
},
showTab(aTab) {
if (!aTab.hidden || aTab == FirefoxViewHandler.tab) {
return;
}
aTab.removeAttribute("hidden");
this.tabContainer._invalidateCachedVisibleTabs();
this.tabContainer._updateCloseButtons();
this.tabContainer._updateHiddenTabsStatus();
this.tabContainer._setPositionalAttributes();
let event = document.createEvent("Events");
event.initEvent("TabShow", true, false);
aTab.dispatchEvent(event);
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
},
hideTab(aTab, aSource) {
if (
aTab.hidden ||
aTab.pinned ||
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
aTab._sharingState?.webRTC?.sharing
) {
return;
}
aTab.setAttribute("hidden", "true");
this.tabContainer._invalidateCachedVisibleTabs();
this.tabContainer._updateCloseButtons();
this.tabContainer._updateHiddenTabsStatus();
this.tabContainer._setPositionalAttributes();
// Splice this tab out of any lines of succession before any events are
// dispatched.
this.replaceInSuccession(aTab, aTab.successor);
this.setSuccessor(aTab, null);
let event = document.createEvent("Events");
event.initEvent("TabHide", true, false);
aTab.dispatchEvent(event);
if (aSource) {
SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource);
}
},
selectTabAtIndex(aIndex, aEvent) {
let tabs = this.visibleTabs;
// count backwards for aIndex < 0
if (aIndex < 0) {
aIndex += tabs.length;
// clamp at index 0 if still negative.
if (aIndex < 0) {
aIndex = 0;
}
} else if (aIndex >= tabs.length) {
// clamp at right-most tab if out of range.
aIndex = tabs.length - 1;
}
this.selectedTab = tabs[aIndex];
if (aEvent) {
aEvent.preventDefault();
aEvent.stopPropagation();
}
},
/**
* Moves a tab to a new browser window, unless it's already the only tab
* in the current window, in which case this will do nothing.
*/
replaceTabWithWindow(aTab, aOptions) {
if (this.tabs.length == 1) {
return null;
}
var options = "chrome,dialog=no,all";
for (var name in aOptions) {
options += "," + name + "=" + aOptions[name];
}
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
options += ",private=1";
}
// Play the tab closing animation to give immediate feedback while
// waiting for the new window to appear.
// content area when the docshells are swapped.
if (!gReduceMotion) {
aTab.style.maxWidth = ""; // ensure that fade-out transition happens
aTab.removeAttribute("fadein");
}
// tell a new window to take the "dropped" tab
return window.openDialog(
AppConstants.BROWSER_CHROME_URL,
"_blank",
options,
aTab
);
},
/**
* Move contextTab (or selected tabs in a mutli-select context)
* to a new browser window, unless it is (they are) already the only tab(s)
* in the current window, in which case this will do nothing.
*/
replaceTabsWithWindow(contextTab, aOptions = {}) {
let tabs;
if (contextTab.multiselected) {
tabs = this.selectedTabs;
} else {
tabs = [contextTab];
}
if (this.tabs.length == tabs.length) {
return null;
}
if (tabs.length == 1) {
return this.replaceTabWithWindow(tabs[0], aOptions);
}
// Play the closing animation for all selected tabs to give
// immediate feedback while waiting for the new window to appear.
if (!gReduceMotion) {
for (let tab of tabs) {
tab.style.maxWidth = ""; // ensure that fade-out transition happens
tab.removeAttribute("fadein");
}
}
// Create a new window and make it adopt the tabs, preserving their relative order.
// The initial tab of the new window will be selected, so it should adopt the
// selected tab of the original window, if applicable, or else the first moving tab.
// This avoids tab-switches in the new window, preserving tab laziness.
// However, to avoid multiple tab-switches in the original window, the other tabs
// should be adopted before the selected one.
let { selectedTab } = gBrowser;
if (!tabs.includes(selectedTab)) {
selectedTab = tabs[0];
}
let win = this.replaceTabWithWindow(selectedTab, aOptions);
win.addEventListener(
"before-initial-tab-adopted",
() => {
let index = 0;
for (let tab of tabs) {
if (tab !== selectedTab) {
const newTab = win.gBrowser.adoptTab(tab, index);
if (!newTab) {
// The adoption failed. Restore "fadein" and don't increase the index.
tab.setAttribute("fadein", "true");
continue;
}
}
++index;
}
// Restore tab selection
let winVisibleTabs = win.gBrowser.visibleTabs;
let winTabLength = winVisibleTabs.length;
win.gBrowser.addRangeToMultiSelectedTabs(
winVisibleTabs[0],
winVisibleTabs[winTabLength - 1]
);
win.gBrowser.lockClearMultiSelectionOnce();
},
{ once: true }
);
return win;
},
_updateTabsAfterInsert() {
for (let i = 0; i < this.tabs.length; i++) {
this.tabs[i]._tPos = i;
this.tabs[i]._selected = false;
}
// If we're in the midst of an async tab switch while calling
// moveTabTo, we can get into a case where _visuallySelected
// is set to true on two different tabs.
//
// What we want to do in moveTabTo is to remove logical selection
// from all tabs, and then re-add logical selection to selectedTab
// (and visual selection as well if we're not running with e10s, which
// setting _selected will do automatically).
//
// If we're running with e10s, then the visual selection will not
// be changed, which is fine, since if we weren't in the midst of a
// tab switch, the previously visually selected tab should still be
// correct, and if we are in the midst of a tab switch, then the async
// tab switcher will set the visually selected tab once the tab switch
// has completed.
this.selectedTab._selected = true;
},
moveTabTo(aTab, aIndex, aKeepRelatedTabs) {
var oldPosition = aTab._tPos;
if (oldPosition == aIndex) {
return;
}
// Don't allow mixing pinned and unpinned tabs.
if (aTab.pinned) {
aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
} else {
aIndex = Math.max(aIndex, this._numPinnedTabs);
}
if (oldPosition == aIndex) {
return;
}
if (!aKeepRelatedTabs) {
this._lastRelatedTabMap = new WeakMap();
}
let wasFocused = document.activeElement == this.selectedTab;
aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;
let neighbor = this.tabs[aIndex] || null;
this.tabContainer._invalidateCachedTabs();
this.tabContainer.insertBefore(aTab, neighbor);
this._updateTabsAfterInsert();
if (wasFocused) {
this.selectedTab.focus();
}
this.tabContainer._handleTabSelect(true);
if (aTab.pinned) {
this.tabContainer._positionPinnedTabs();
}
this.tabContainer._setPositionalAttributes();
var evt = document.createEvent("UIEvents");
evt.initUIEvent("TabMove", true, false, window, oldPosition);
aTab.dispatchEvent(evt);
},
moveTabForward() {
let nextTab = this.tabContainer.findNextTab(this.selectedTab, {
direction: 1,
filter: tab => !tab.hidden,
});
if (nextTab) {
this.moveTabTo(this.selectedTab, nextTab._tPos);
} else if (this.arrowKeysShouldWrap) {
this.moveTabToStart();
}
},
/**
* Adopts a tab from another browser window, and inserts it at aIndex
*
* @returns {object}
* The new tab in the current window, null if the tab couldn't be adopted.
*/
adoptTab(aTab, aIndex, aSelectTab) {
// Swap the dropped tab with a new one we create and then close
// it in the other window (making it seem to have moved between
// windows). We also ensure that the tab we create to swap into has
// the same remote type and process as the one we're swapping in.
// This makes sure we don't get a short-lived process for the new tab.
let linkedBrowser = aTab.linkedBrowser;
let createLazyBrowser = !aTab.linkedPanel;
let params = {
eventDetail: { adoptedTab: aTab },
preferredRemoteType: linkedBrowser.remoteType,
initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id,
skipAnimation: true,
index: aIndex,
createLazyBrowser,
};
let numPinned = this._numPinnedTabs;
if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
params.pinned = true;
}
if (aTab.hasAttribute("usercontextid")) {
// new tab must have the same usercontextid as the old one
params.userContextId = aTab.getAttribute("usercontextid");
}
let newTab = this.addWebTab("about:blank", params);
let newBrowser = this.getBrowserForTab(newTab);
aTab.container._finishAnimateTabMove();
if (!createLazyBrowser) {
// Stop the about:blank load.
newBrowser.stop();
// Make sure it has a docshell.
newBrowser.docShell;
}
if (!this.swapBrowsersAndCloseOther(newTab, aTab)) {
// Swapping wasn't permitted. Bail out.
this.removeTab(newTab);
return null;
}
if (aSelectTab) {
this.selectedTab = newTab;
}
return newTab;
},
moveTabBackward() {
let previousTab = this.tabContainer.findNextTab(this.selectedTab, {
direction: -1,
filter: tab => !tab.hidden,
});
if (previousTab) {
this.moveTabTo(this.selectedTab, previousTab._tPos);
} else if (this.arrowKeysShouldWrap) {
this.moveTabToEnd();
}
},
moveTabToStart() {
let tabPos = this.selectedTab._tPos;
if (tabPos > 0) {
this.moveTabTo(this.selectedTab, 0);
}
},
moveTabToEnd() {
let tabPos = this.selectedTab._tPos;
if (tabPos < this.browsers.length - 1) {
this.moveTabTo(this.selectedTab, this.browsers.length - 1);
}
},
moveTabOver(aEvent) {
if (
(!RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
(RTL_UI && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)
) {
this.moveTabForward();
} else {
this.moveTabBackward();
}
},
/**
* @param aTab
* Can be from a different window as well
* @param aRestoreTabImmediately
* Can defer loading of the tab contents
* @param aOptions
* The new index of the tab
*/
duplicateTab(aTab, aRestoreTabImmediately, aOptions) {
return SessionStore.duplicateTab(
window,
aTab,
0,
aRestoreTabImmediately,
aOptions
);
},
addToMultiSelectedTabs(aTab) {
if (aTab.multiselected) {
return;
}
aTab.setAttribute("multiselected", "true");
aTab.setAttribute("aria-selected", "true");
this._multiSelectedTabsSet.add(aTab);
this._startMultiSelectChange();
if (this._multiSelectChangeRemovals.has(aTab)) {
this._multiSelectChangeRemovals.delete(aTab);
} else {
this._multiSelectChangeAdditions.add(aTab);
}
},
/**
* Adds two given tabs and all tabs between them into the (multi) selected tabs collection
*/
addRangeToMultiSelectedTabs(aTab1, aTab2) {
if (aTab1 == aTab2) {
return;
}
const tabs = this.visibleTabs;
const indexOfTab1 = tabs.indexOf(aTab1);
const indexOfTab2 = tabs.indexOf(aTab2);
const [lowerIndex, higherIndex] =
indexOfTab1 < indexOfTab2
? [Math.max(0, indexOfTab1), indexOfTab2]
: [Math.max(0, indexOfTab2), indexOfTab1];
for (let i = lowerIndex; i <= higherIndex; i++) {
this.addToMultiSelectedTabs(tabs[i]);
}
},
removeFromMultiSelectedTabs(aTab) {
if (!aTab.multiselected) {
return;
}
aTab.removeAttribute("multiselected");
aTab.removeAttribute("aria-selected");
this._multiSelectedTabsSet.delete(aTab);
this._startMultiSelectChange();
if (this._multiSelectChangeAdditions.has(aTab)) {
this._multiSelectChangeAdditions.delete(aTab);
} else {
this._multiSelectChangeRemovals.add(aTab);
}
},
clearMultiSelectedTabs() {
if (this._clearMultiSelectionLocked) {
if (this._clearMultiSelectionLockedOnce) {
this._clearMultiSelectionLockedOnce = false;
this._clearMultiSelectionLocked = false;
}
return;
}
if (this.multiSelectedTabsCount < 1) {
return;
}
for (let tab of this.selectedTabs) {
this.removeFromMultiSelectedTabs(tab);
}
this._lastMultiSelectedTabRef = null;
},
selectAllTabs() {
let visibleTabs = this.visibleTabs;
gBrowser.addRangeToMultiSelectedTabs(
visibleTabs[0],
visibleTabs[visibleTabs.length - 1]
);
},
allTabsSelected() {
return (
this.visibleTabs.length == 1 ||
this.visibleTabs.every(t => t.multiselected)
);
},
lockClearMultiSelectionOnce() {
this._clearMultiSelectionLockedOnce = true;
this._clearMultiSelectionLocked = true;
},
unlockClearMultiSelection() {
this._clearMultiSelectionLockedOnce = false;
this._clearMultiSelectionLocked = false;
},
/**
* Remove a tab from the multiselection if it's the only one left there.
*
* In fact, some scenario may lead to only one single tab multi-selected,
* this is something to avoid (Chrome does the same)
* Consider 4 tabs A,B,C,D with A having the focus
* 1. select C with Ctrl
* 2. Right-click on B and "Close Tabs to The Right"
*
* Expected result
* C and D closing
* A being the only multi-selected tab, selection should be cleared
*
*
* Single selected tab could even happen with a none-focused tab.
* For exemple with the menu "Close other tabs", it could happen
* with a multi-selected pinned tab.
* For illustration, consider 4 tabs A,B,C,D with B active
* 1. pin A and Ctrl-select it
* 2. Ctrl-select C
* 3. right-click on D and click "Close Other Tabs"
*
* Expected result
* B and C closing
* A[pinned] being the only multi-selected tab, selection should be cleared.
*/
_avoidSingleSelectedTab() {
if (this.multiSelectedTabsCount == 1) {
this.clearMultiSelectedTabs();
}
},
_switchToNextMultiSelectedTab() {
this._clearMultiSelectionLocked = true;
// Guarantee that _clearMultiSelectionLocked lock gets released.
try {
let lastMultiSelectedTab = this.lastMultiSelectedTab;
if (!lastMultiSelectedTab.selected) {
this.selectedTab = lastMultiSelectedTab;
} else {
let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(
this._multiSelectedTabsSet
).filter(this._mayTabBeMultiselected);
this.selectedTab = selectedTabs.at(-1);
}
} catch (e) {
console.error(e);
}
this._clearMultiSelectionLocked = false;
},
set selectedTabs(tabs) {
this.clearMultiSelectedTabs();
this.selectedTab = tabs[0];
if (tabs.length > 1) {
for (let tab of tabs) {
this.addToMultiSelectedTabs(tab);
}
}
},
get selectedTabs() {
let { selectedTab, _multiSelectedTabsSet } = this;
let tabs = ChromeUtils.nondeterministicGetWeakSetKeys(
_multiSelectedTabsSet
).filter(this._mayTabBeMultiselected);
if (
(!_multiSelectedTabsSet.has(selectedTab) &&
this._mayTabBeMultiselected(selectedTab)) ||
!tabs.length
) {
tabs.push(selectedTab);
}
return tabs.sort((a, b) => a._tPos > b._tPos);
},
get multiSelectedTabsCount() {
return ChromeUtils.nondeterministicGetWeakSetKeys(
this._multiSelectedTabsSet
).filter(this._mayTabBeMultiselected).length;
},
get lastMultiSelectedTab() {
let tab = this._lastMultiSelectedTabRef
? this._lastMultiSelectedTabRef.get()
: null;
if (tab && tab.isConnected && this._multiSelectedTabsSet.has(tab)) {
return tab;
}
let selectedTab = this.selectedTab;
this.lastMultiSelectedTab = selectedTab;
return selectedTab;
},
set lastMultiSelectedTab(aTab) {
this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
},
_mayTabBeMultiselected(aTab) {
return aTab.isConnected && !aTab.closing && !aTab.hidden;
},
_startMultiSelectChange() {
if (!this._multiSelectChangeStarted) {
this._multiSelectChangeStarted = true;
Promise.resolve().then(() => this._endMultiSelectChange());
}
},
_endMultiSelectChange() {
let noticeable = false;
let { selectedTab } = this;
if (this._multiSelectChangeAdditions.size) {
if (!selectedTab.multiselected) {
this.addToMultiSelectedTabs(selectedTab);
}
noticeable = true;
}
if (this._multiSelectChangeRemovals.size) {
if (this._multiSelectChangeRemovals.has(selectedTab)) {
this._switchToNextMultiSelectedTab();
}
this._avoidSingleSelectedTab();
noticeable = true;
}
this._multiSelectChangeStarted = false;
if (noticeable || this._multiSelectChangeSelected) {
this._multiSelectChangeSelected = false;
this._multiSelectChangeAdditions.clear();
this._multiSelectChangeRemovals.clear();
if (noticeable) {
this.tabContainer._setPositionalAttributes();
}
this.dispatchEvent(
new CustomEvent("TabMultiSelect", { bubbles: true })
);
}
},
toggleMuteAudioOnMultiSelectedTabs(aTab) {
let tabMuted = aTab.linkedBrowser.audioMuted;
let tabsToToggle = this.selectedTabs.filter(
tab => tab.linkedBrowser.audioMuted == tabMuted
);
for (let tab of tabsToToggle) {
tab.toggleMuteAudio();
}
},
resumeDelayedMediaOnMultiSelectedTabs() {
for (let tab of this.selectedTabs) {
tab.resumeDelayedMedia();
}
},
pinMultiSelectedTabs() {
for (let tab of this.selectedTabs) {
this.pinTab(tab);
}
},
unpinMultiSelectedTabs() {
// The selectedTabs getter returns the tabs
// in visual order. We need to unpin in reverse
// order to maintain visual order.
let selectedTabs = this.selectedTabs;
for (let i = selectedTabs.length - 1; i >= 0; i--) {
let tab = selectedTabs[i];
this.unpinTab(tab);
}
},
activateBrowserForPrintPreview(aBrowser) {
this._printPreviewBrowsers.add(aBrowser);
if (this._switcher) {
this._switcher.activateBrowserForPrintPreview(aBrowser);
}
aBrowser.docShellIsActive = true;
},
deactivatePrintPreviewBrowsers() {
let browsers = this._printPreviewBrowsers;
this._printPreviewBrowsers = new Set();
for (let browser of browsers) {
browser.docShellIsActive = this.shouldActivateDocShell(browser);
}
},
/**
* Returns true if a given browser's docshell should be active.
*/
shouldActivateDocShell(aBrowser) {
if (this._switcher) {
return this._switcher.shouldActivateDocShell(aBrowser);
}
return (
(aBrowser == this.selectedBrowser && !document.hidden) ||
this._printPreviewBrowsers.has(aBrowser) ||
this.PictureInPicture.isOriginatingBrowser(aBrowser)
);
},
_getSwitcher() {
if (!this._switcher) {
this._switcher = new this.AsyncTabSwitcher(this);
}
return this._switcher;
},
warmupTab(aTab) {
if (gMultiProcessBrowser) {
this._getSwitcher().warmupTab(aTab);
}
},
/**
* _maybeRequestReplyFromRemoteContent may call
* aEvent.requestReplyFromRemoteContent if necessary.
*
* @param aEvent The handling event.
* @return true if the handler should wait a reply event.
* false if the handle can handle the immediately.
*/
_maybeRequestReplyFromRemoteContent(aEvent) {
if (aEvent.defaultPrevented) {
return false;
}
// If the event target is a remote browser, and the event has not been
// handled by the remote content yet, we should wait a reply event
// from the content.
if (aEvent.isWaitingReplyFromRemoteContent) {
return true; // Somebody called requestReplyFromRemoteContent already.
}
if (
!aEvent.isReplyEventFromRemoteContent &&
aEvent.target?.isRemoteBrowser === true
) {
aEvent.requestReplyFromRemoteContent();
return true;
}
return false;
},
_handleKeyDownEvent(aEvent) {
if (!aEvent.isTrusted) {
// Don't let untrusted events mess with tabs.
return;
}
// Skip this only if something has explicitly cancelled it.
if (aEvent.defaultCancelled) {
return;
}
// Skip if chrome code has cancelled this:
if (aEvent.defaultPreventedByChrome) {
return;
}
// Don't check if the event was already consumed because tab
// navigation should always work for better user experience.
switch (ShortcutUtils.getSystemActionForEvent(aEvent)) {
case ShortcutUtils.TOGGLE_CARET_BROWSING:
this._maybeRequestReplyFromRemoteContent(aEvent);
return;
case ShortcutUtils.MOVE_TAB_BACKWARD:
this.moveTabBackward();
aEvent.preventDefault();
return;
case ShortcutUtils.MOVE_TAB_FORWARD:
this.moveTabForward();
aEvent.preventDefault();
return;
case ShortcutUtils.CLOSE_TAB:
if (gBrowser.multiSelectedTabsCount) {
gBrowser.removeMultiSelectedTabs();
} else if (!this.selectedTab.pinned) {
this.removeCurrentTab({ animate: true });
}
aEvent.preventDefault();
}
},
toggleCaretBrowsing() {
const kPrefShortcutEnabled =
"accessibility.browsewithcaret_shortcut.enabled";
const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
var isEnabled = Services.prefs.getBoolPref(kPrefShortcutEnabled);
if (!isEnabled || this._awaitingToggleCaretBrowsingPrompt) {
return;
}
// Toggle browse with caret mode
var browseWithCaretOn = Services.prefs.getBoolPref(
kPrefCaretBrowsingOn,
false
);
var warn = Services.prefs.getBoolPref(kPrefWarnOnEnable, true);
if (warn && !browseWithCaretOn) {
var checkValue = { value: false };
var promptService = Services.prompt;
try {
this._awaitingToggleCaretBrowsingPrompt = true;
const [
title,
message,
checkbox,
] = this.tabLocalization.formatValuesSync([
"tabbrowser-confirm-caretbrowsing-title",
"tabbrowser-confirm-caretbrowsing-message",
"tabbrowser-confirm-caretbrowsing-checkbox",
]);
var buttonPressed = promptService.confirmEx(
window,
title,
message,
// Make "No" the default:
promptService.STD_YES_NO_BUTTONS |
promptService.BUTTON_POS_1_DEFAULT,
null,
null,
null,
checkbox,
checkValue
);
} catch (ex) {
return;
} finally {
this._awaitingToggleCaretBrowsingPrompt = false;
}
if (buttonPressed != 0) {
if (checkValue.value) {
try {
Services.prefs.setBoolPref(kPrefShortcutEnabled, false);
} catch (ex) {}
}
return;
}
if (checkValue.value) {
try {
Services.prefs.setBoolPref(kPrefWarnOnEnable, false);
} catch (ex) {}
}
}
// Toggle the pref
try {
Services.prefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
} catch (ex) {}
},
_handleKeyPressEvent(aEvent) {
if (!aEvent.isTrusted) {
// Don't let untrusted events mess with tabs.
return;
}
// Skip this only if something has explicitly cancelled it.
if (aEvent.defaultCancelled) {
return;
}
// Skip if chrome code has cancelled this:
if (aEvent.defaultPreventedByChrome) {
return;
}
switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) {
case ShortcutUtils.TOGGLE_CARET_BROWSING:
if (
aEvent.defaultPrevented ||
this._maybeRequestReplyFromRemoteContent(aEvent)
) {
break;
}
this.toggleCaretBrowsing();
break;
case ShortcutUtils.NEXT_TAB:
if (AppConstants.platform == "macosx") {
this.tabContainer.advanceSelectedTab(1, true);
aEvent.preventDefault();
}
break;
case ShortcutUtils.PREVIOUS_TAB:
if (AppConstants.platform == "macosx") {
this.tabContainer.advanceSelectedTab(-1, true);
aEvent.preventDefault();
}
break;
}
},
getTabTooltip(tab, includeLabel = true) {
let label = "";
if (includeLabel) {
label = tab._fullLabel || tab.getAttribute("label");
}
if (
Services.prefs.getBoolPref(
"browser.tabs.tooltipsShowPidAndActiveness",
false
)
) {
if (tab.linkedBrowser) {
// Show the PIDs of the content process and remote subframe processes.
let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
tab.linkedBrowser,
gFissionBrowser
);
if (contentPid) {
if (framePids && framePids.length) {
label += ` (pids ${contentPid}, ${framePids.sort().join(", ")})`;
} else {
label += ` (pid ${contentPid})`;
}
}
if (tab.linkedBrowser.docShellIsActive) {
label += " [A]";
}
}
}
if (tab.userContextId) {
const containerName = ContextualIdentityService.getUserContextLabel(
tab.userContextId
);
label = this.tabLocalization.formatValueSync(
"tabbrowser-container-tab-title",
{ title: label, containerName }
);
}
return label;
},
createTooltip(event) {
event.stopPropagation();
let tab = event.target.triggerNode?.closest("tab");
if (!tab) {
event.preventDefault();
return;
}
let l10nId, l10nArgs;
const tabCount = this.selectedTabs.includes(tab)
? this.selectedTabs.length
: 1;
if (tab.mOverCloseButton) {
l10nId = "tabbrowser-close-tabs-tooltip";
l10nArgs = { tabCount };
} else if (tab._overPlayingIcon) {
l10nArgs = { tabCount };
if (tab.selected) {
l10nId = tab.linkedBrowser.audioMuted
? "tabbrowser-unmute-tab-audio-tooltip"
: "tabbrowser-mute-tab-audio-tooltip";
const keyElem = document.getElementById("key_toggleMute");
l10nArgs.shortcut = ShortcutUtils.prettifyShortcut(keyElem);
} else if (tab.hasAttribute("activemedia-blocked")) {
l10nId = "tabbrowser-unblock-tab-audio-tooltip";
} else {
l10nId = tab.linkedBrowser.audioMuted
? "tabbrowser-unmute-tab-audio-background-tooltip"
: "tabbrowser-mute-tab-audio-background-tooltip";
}
} else {
l10nId = "tabbrowser-tab-tooltip";
l10nArgs = { title: this.getTabTooltip(tab, true) };
}
document.l10n.setAttributes(event.target, l10nId, l10nArgs);
},
handleEvent(aEvent) {
switch (aEvent.type) {
case "keydown":
this._handleKeyDownEvent(aEvent);
break;
case "keypress":
this._handleKeyPressEvent(aEvent);
break;
case "framefocusrequested": {
let tab = this.getTabForBrowser(aEvent.target);
if (!tab || tab == this.selectedTab) {
// Let the focus manager try to do its thing by not calling
// preventDefault(). It will still raise the window if appropriate.
break;
}
this.selectedTab = tab;
window.focus();
aEvent.preventDefault();
break;
}
case "visibilitychange":
const inactive = document.hidden;
if (!this._switcher) {
this.selectedBrowser.preserveLayers(inactive);
this.selectedBrowser.docShellIsActive = !inactive;
}
break;
}
},
observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "contextual-identity-updated": {
let identity = aSubject.wrappedJSObject;
for (let tab of this.tabs) {
if (tab.getAttribute("usercontextid") == identity.userContextId) {
ContextualIdentityService.setTabStyle(tab);
}
}
break;
}
}
},
refreshBlocked(actor, browser, data) {
// The data object is expected to contain the following properties:
// - URI (string)
// The URI that a page is attempting to refresh or redirect to.
// - delay (int)
// The delay (in milliseconds) before the page was going to
// reload or redirect.
// - sameURI (bool)
// true if we're refreshing the page. false if we're redirecting.
let notificationBox = this.getNotificationBox(browser);
let notification = notificationBox.getNotificationWithValue(
"refresh-blocked"
);
let l10nId = data.sameURI
? "refresh-blocked-refresh-label"
: "refresh-blocked-redirect-label";
if (notification) {
notification.label = { "l10n-id": l10nId };
} else {
const buttons = [
{
"l10n-id": "refresh-blocked-allow",
callback() {
actor.sendAsyncMessage("RefreshBlocker:Refresh", data);
},
},
];
notificationBox.appendNotification(
"refresh-blocked",
{
label: { "l10n-id": l10nId },
image: "chrome://browser/skin/notification-icons/popup.svg",
priority: notificationBox.PRIORITY_INFO_MEDIUM,
},
buttons
);
}
},
_generateUniquePanelID() {
if (!this._uniquePanelIDCounter) {
this._uniquePanelIDCounter = 0;
}
let outerID = window.docShell.outerWindowID;
// We want panel IDs to be globally unique, that's why we include the
// window ID. We switched to a monotonic counter as Date.now() lead
// to random failures because of colliding IDs.
return "panel-" + outerID + "-" + ++this._uniquePanelIDCounter;
},
destroy() {
this.tabContainer.destroy();
Services.obs.removeObserver(this, "contextual-identity-updated");
for (let tab of this.tabs) {
let browser = tab.linkedBrowser;
if (browser.registeredOpenURI) {
let userContextId = browser.getAttribute("usercontextid") || 0;
this.UrlbarProviderOpenTabs.unregisterOpenTab(
browser.registeredOpenURI.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
);
delete browser.registeredOpenURI;
}
let filter = this._tabFilters.get(tab);
if (filter) {
browser.webProgress.removeProgressListener(filter);
let listener = this._tabListeners.get(tab);
if (listener) {
filter.removeProgressListener(listener);
listener.destroy();
}
this._tabFilters.delete(tab);
this._tabListeners.delete(tab);
}
}
Services.els.removeSystemEventListener(document, "keydown", this, false);
if (AppConstants.platform == "macosx") {
Services.els.removeSystemEventListener(
document,
"keypress",
this,
false
);
}
document.removeEventListener("visibilitychange", this);
window.removeEventListener("framefocusrequested", this);
if (gMultiProcessBrowser) {
if (this._switcher) {
this._switcher.destroy();
}
}
},
_setupEventListeners() {
this.tabpanels.addEventListener("select", event => {
if (event.target == this.tabpanels) {
this.updateCurrentBrowser();
}
});
this.addEventListener("DOMWindowClose", event => {
let browser = event.target;
if (!browser.isRemoteBrowser) {
if (!event.isTrusted) {
// If the browser is not remote, then we expect the event to be trusted.
// In the remote case, the DOMWindowClose event is captured in content,
// a message is sent to the parent, and another DOMWindowClose event
// is re-dispatched on the actual browser node. In that case, the event
// won't be marked as trusted, since it's synthesized by JavaScript.
return;
}
// In the parent-process browser case, it's possible that the browser
// that fired DOMWindowClose is actually a child of another browser. We
// want to find the top-most browser to determine whether or not this is
// for a tab or not. The chromeEventHandler will be the top-most browser.
browser = event.target.docShell.chromeEventHandler;
}
if (this.tabs.length == 1) {
// We already did PermitUnload in the content process
// for this tab (the only one in the window). So we don't
// need to do it again for any tabs.
window.skipNextCanClose = true;
// In the parent-process browser case, the nsCloseEvent will actually take
// care of tearing down the window, but we need to do this ourselves in the
// content-process browser case. Doing so in both cases doesn't appear to
// hurt.
window.close();
return;
}
let tab = this.getTabForBrowser(browser);
if (tab) {
// Skip running PermitUnload since it already happened in
// the content process.
this.removeTab(tab, { skipPermitUnload: true });
// If we don't preventDefault on the DOMWindowClose event, then
// in the parent-process browser case, we're telling the platform
// to close the entire window. Calling preventDefault is our way of
// saying we took care of this close request by closing the tab.
event.preventDefault();
}
});
this.addEventListener("pagetitlechanged", event => {
let browser = event.target;
let tab = this.getTabForBrowser(browser);
if (!tab || tab.hasAttribute("pending")) {
return;
}
// Ignore empty title changes on internal pages. This prevents the title
// from changing while Fluent is populating the (initially-empty) title
// element.
if (
!browser.contentTitle &&
browser.contentPrincipal.isSystemPrincipal
) {
return;
}
let titleChanged = this.setTabTitle(tab);
if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
tab.setAttribute("titlechanged", "true");
}
});
this.addEventListener(
"DOMWillOpenModalDialog",
event => {
if (!event.isTrusted) {
return;
}
let targetIsWindow = Window.isInstance(event.target);
// We're about to open a modal dialog, so figure out for which tab:
// If this is a same-process modal dialog, then we're given its DOM
// window as the event's target. For remote dialogs, we're given the
// browser, but that's in the originalTarget and not the target,
// because it's across the tabbrowser's XBL boundary.
let tabForEvent = targetIsWindow
? this.getTabForBrowser(event.target.docShell.chromeEventHandler)
: this.getTabForBrowser(event.originalTarget);
// Focus window for beforeunload dialog so it is seen but don't
// steal focus from other applications.
if (
event.detail &&
event.detail.tabPrompt &&
event.detail.inPermitUnload &&
Services.focus.activeWindow
) {
window.focus();
}
// Don't need to act if the tab is already selected or if there isn't
// a tab for the event (e.g. for the webextensions options_ui remote
// browsers embedded in the "about:addons" page):
if (!tabForEvent || tabForEvent.selected) {
return;
}
// We always switch tabs for beforeunload tab-modal prompts.
if (
event.detail &&
event.detail.tabPrompt &&
!event.detail.inPermitUnload
) {
let docPrincipal = targetIsWindow
? event.target.document.nodePrincipal
: null;
// At least one of these should/will be non-null:
let promptPrincipal =
event.detail.promptPrincipal ||
docPrincipal ||
tabForEvent.linkedBrowser.contentPrincipal;
// For null principals, we bail immediately and don't show the checkbox:
if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
tabForEvent.attention = true;
return;
}
// For non-system/expanded principals without permission, we bail and show the checkbox.
if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) {
let permission = Services.perms.testPermissionFromPrincipal(
promptPrincipal,
"focus-tab-by-prompt"
);
if (permission != Services.perms.ALLOW_ACTION) {
// Tell the prompt box we want to show the user a checkbox:
let tabPrompt = Services.prefs.getBoolPref(
"prompts.contentPromptSubDialog"
)
? this.getTabDialogBox(tabForEvent.linkedBrowser)
: this.getTabModalPromptBox(tabForEvent.linkedBrowser);
tabPrompt.onNextPromptShowAllowFocusCheckboxFor(
promptPrincipal
);
tabForEvent.attention = true;
return;
}
}
// ... so system and expanded principals, as well as permitted "normal"
// URI-based principals, always get to steal focus for the tab when prompting.
}
// If permissions/origins dictate so, bring tab to the front.
this.selectedTab = tabForEvent;
},
true
);
// When cancelling beforeunload tabmodal dialogs, reset the URL bar to
// avoid spoofing risks.
this.addEventListener(
"DOMModalDialogClosed",
event => {
if (
!event.detail?.wasPermitUnload ||
event.detail.areLeaving ||
event.target.nodeName != "browser"
) {
return;
}
event.target.userTypedValue = null;
if (event.target == this.selectedBrowser) {
gURLBar.setURI();
}
},
true
);
let onTabCrashed = event => {
if (!event.isTrusted) {
return;
}
let browser = event.originalTarget;
if (!event.isTopFrame) {
TabCrashHandler.onSubFrameCrash(browser, event.childID);
return;
}
// Preloaded browsers do not actually have any tabs. If one crashes,
// it should be released and removed.
if (browser === this.preloadedBrowser) {
NewTabPagePreloading.removePreloadedBrowser(window);
return;
}
let isRestartRequiredCrash =
event.type == "oop-browser-buildid-mismatch";
let icon = browser.mIconURL;
let tab = this.getTabForBrowser(browser);
if (this.selectedBrowser == browser) {
TabCrashHandler.onSelectedBrowserCrash(
browser,
isRestartRequiredCrash
);
} else {
TabCrashHandler.onBackgroundBrowserCrash(
browser,
isRestartRequiredCrash
);
}
tab.removeAttribute("soundplaying");
this.setIcon(tab, icon);
};
this.addEventListener("oop-browser-crashed", onTabCrashed);
this.addEventListener("oop-browser-buildid-mismatch", onTabCrashed);
this.addEventListener("DOMAudioPlaybackStarted", event => {
var tab = this.getTabFromAudioEvent(event);
if (!tab) {
return;
}
clearTimeout(tab._soundPlayingAttrRemovalTimer);
tab._soundPlayingAttrRemovalTimer = 0;
let modifiedAttrs = [];
if (tab.hasAttribute("soundplaying-scheduledremoval")) {
tab.removeAttribute("soundplaying-scheduledremoval");
modifiedAttrs.push("soundplaying-scheduledremoval");
}
if (!tab.hasAttribute("soundplaying")) {
tab.setAttribute("soundplaying", true);
modifiedAttrs.push("soundplaying");
}
if (modifiedAttrs.length) {
// Flush style so that the opacity takes effect immediately, in
// case the media is stopped before the style flushes naturally.
getComputedStyle(tab).opacity;
}
this._tabAttrModified(tab, modifiedAttrs);
});
this.addEventListener("DOMAudioPlaybackStopped", event => {
var tab = this.getTabFromAudioEvent(event);
if (!tab) {
return;
}
if (tab.hasAttribute("soundplaying")) {
let removalDelay = Services.prefs.getIntPref(
"browser.tabs.delayHidingAudioPlayingIconMS"
);
tab.style.setProperty(
"--soundplaying-removal-delay",
`${removalDelay - 300}ms`
);
tab.setAttribute("soundplaying-scheduledremoval", "true");
this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
tab.removeAttribute("soundplaying-scheduledremoval");
tab.removeAttribute("soundplaying");
this._tabAttrModified(tab, [
"soundplaying",
"soundplaying-scheduledremoval",
]);
}, removalDelay);
}
});
this.addEventListener("DOMAudioPlaybackBlockStarted", event => {
var tab = this.getTabFromAudioEvent(event);
if (!tab) {
return;
}
if (!tab.hasAttribute("activemedia-blocked")) {
tab.setAttribute("activemedia-blocked", true);
this._tabAttrModified(tab, ["activemedia-blocked"]);
}
});
this.addEventListener("DOMAudioPlaybackBlockStopped", event => {
var tab = this.getTabFromAudioEvent(event);
if (!tab) {
return;
}
if (tab.hasAttribute("activemedia-blocked")) {
tab.removeAttribute("activemedia-blocked");
this._tabAttrModified(tab, ["activemedia-blocked"]);
let hist = Services.telemetry.getHistogramById(
"TAB_AUDIO_INDICATOR_USED"
);
hist.add(2 /* unblockByVisitingTab */);
}
});
this.addEventListener("GloballyAutoplayBlocked", event => {
let browser = event.originalTarget;
let tab = this.getTabForBrowser(browser);
if (!tab) {
return;
}
SitePermissions.setForPrincipal(
browser.contentPrincipal,
"autoplay-media",
SitePermissions.BLOCK,
SitePermissions.SCOPE_GLOBAL,
browser
);
});
let tabContextFTLInserter = () => {
this.translateTabContextMenu();
this.tabContainer.removeEventListener(
"contextmenu",
tabContextFTLInserter,
true
);
this.tabContainer.removeEventListener(
"mouseover",
tabContextFTLInserter
);
this.tabContainer.removeEventListener(
"focus",
tabContextFTLInserter,
true
);
};
this.tabContainer.addEventListener(
"contextmenu",
tabContextFTLInserter,
true
);
this.tabContainer.addEventListener("mouseover", tabContextFTLInserter);
this.tabContainer.addEventListener("focus", tabContextFTLInserter, true);
// Fired when Gecko has decided a <browser> element will change
// remoteness. This allows persisting some state on this element across
// process switches.
this.addEventListener("WillChangeBrowserRemoteness", event => {
let browser = event.originalTarget;
let tab = this.getTabForBrowser(browser);
if (!tab) {
return;
}
// Dispatch the `BeforeTabRemotenessChange` event, allowing other code
// to react to this tab's process switch.
let evt = document.createEvent("Events");
evt.initEvent("BeforeTabRemotenessChange", true, false);
tab.dispatchEvent(evt);
let wasActive = document.activeElement == browser;
// Unhook our progress listener.
let filter = this._tabFilters.get(tab);
let oldListener = this._tabListeners.get(tab);
browser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(oldListener);
let stateFlags = oldListener.mStateFlags;
let requestCount = oldListener.mRequestCount;
// We'll be creating a new listener, so destroy the old one.
oldListener.destroy();
let oldDroppedLinkHandler = browser.droppedLinkHandler;
let oldUserTypedValue = browser.userTypedValue;
let hadStartedLoad = browser.didStartLoadSinceLastUserTyping();
let didChange = didChangeEvent => {
browser.userTypedValue = oldUserTypedValue;
if (hadStartedLoad) {
browser.urlbarChangeTracker.startedLoad();
}
browser.droppedLinkHandler = oldDroppedLinkHandler;
// This shouldn't really be necessary, however, this has the side effect
// of sending MozLayerTreeReady / MozLayerTreeCleared events for remote
// frames, which the tab switcher depends on.
//
// eslint-disable-next-line no-self-assign
browser.docShellIsActive = browser.docShellIsActive;
// Create a new tab progress listener for the new browser we just
// injected, since tab progress listeners have logic for handling the
// initial about:blank load
let listener = new TabProgressListener(
tab,
browser,
false,
false,
stateFlags,
requestCount
);
this._tabListeners.set(tab, listener);
filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
// Restore the progress listener.
browser.webProgress.addProgressListener(
filter,
Ci.nsIWebProgress.NOTIFY_ALL
);
let cbEvent = browser.getContentBlockingEvents();
// Include the true final argument to indicate that this event is
// simulated (instead of being observed by the webProgressListener).
this._callProgressListeners(
browser,
"onContentBlockingEvent",
[browser.webProgress, null, cbEvent, true],
true,
false
);
if (browser.isRemoteBrowser) {
// Switching the browser to be remote will connect to a new child
// process so the browser can no longer be considered to be
// crashed.
tab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(tab);
} else {
browser.sendMessageToActor(
"Browser:AppTab",
{ isAppTab: tab.pinned },
"BrowserTab"
);
}
if (wasActive) {
browser.focus();
}
if (this.isFindBarInitialized(tab)) {
this.getCachedFindBar(tab).browser = browser;
}
browser.sendMessageToActor(
"Browser:HasSiblings",
this.tabs.length > 1,
"BrowserTab"
);
evt = document.createEvent("Events");
evt.initEvent("TabRemotenessChange", true, false);
tab.dispatchEvent(evt);
};
browser.addEventListener("DidChangeBrowserRemoteness", didChange, {
once: true,
});
});
this.addEventListener("pageinfo", event => {
let browser = event.originalTarget;
let tab = this.getTabForBrowser(browser);
if (!tab) {
return;
}
const { url, description, previewImageURL } = event.detail;
this.setPageInfo(url, description, previewImageURL);
});
},
translateTabContextMenu() {
if (this._tabContextMenuTranslated) {
return;
}
MozXULElement.insertFTLIfNeeded("browser/tabContextMenu.ftl");
// Un-lazify the l10n-ids now that the FTL file has been inserted.
document
.getElementById("tabContextMenu")
.querySelectorAll("[data-lazy-l10n-id]")
.forEach(el => {
el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
el.removeAttribute("data-lazy-l10n-id");
});
this._tabContextMenuTranslated = true;
},
setSuccessor(aTab, successorTab) {
if (aTab.ownerGlobal != window) {
throw new Error("Cannot set the successor of another window's tab");
}
if (successorTab == aTab) {
successorTab = null;
}
if (successorTab && successorTab.ownerGlobal != window) {
throw new Error("Cannot set the successor to another window's tab");
}
if (aTab.successor) {
aTab.successor.predecessors.delete(aTab);
}
aTab.successor = successorTab;
if (successorTab) {
if (!successorTab.predecessors) {
successorTab.predecessors = new Set();
}
successorTab.predecessors.add(aTab);
}
},
/**
* For all tabs with aTab as a successor, set the successor to aOtherTab
* instead.
*/
replaceInSuccession(aTab, aOtherTab) {
if (aTab.predecessors) {
for (const predecessor of Array.from(aTab.predecessors)) {
this.setSuccessor(predecessor, aOtherTab);
}
}
},
};
/**
* A web progress listener object definition for a given tab.
*/
class TabProgressListener {
constructor(
aTab,
aBrowser,
aStartsBlank,
aWasPreloadedBrowser,
aOrigStateFlags,
aOrigRequestCount
) {
let stateFlags = aOrigStateFlags || 0;
// Initialize mStateFlags to non-zero e.g. when creating a progress
// listener for preloaded browsers as there was no progress listener
// around when the content started loading. If the content didn't
// quite finish loading yet, mStateFlags will very soon be overridden
// with the correct value and end up at STATE_STOP again.
if (aWasPreloadedBrowser) {
stateFlags =
Ci.nsIWebProgressListener.STATE_STOP |
Ci.nsIWebProgressListener.STATE_IS_REQUEST;
}
this.mTab = aTab;
this.mBrowser = aBrowser;
this.mBlank = aStartsBlank;
// cache flags for correct status UI update after tab switching
this.mStateFlags = stateFlags;
this.mStatus = 0;
this.mMessage = "";
this.mTotalProgress = 0;
// count of open requests (should always be 0 or 1)
this.mRequestCount = aOrigRequestCount || 0;
}
destroy() {
delete this.mTab;
delete this.mBrowser;
}
_callProgressListeners(...args) {
args.unshift(this.mBrowser);
return gBrowser._callProgressListeners.apply(gBrowser, args);
}
_shouldShowProgress(aRequest) {
if (this.mBlank) {
return false;
}
// Don't show progress indicators in tabs for about: URIs
// pointing to local resources.
if (
aRequest instanceof Ci.nsIChannel &&
aRequest.originalURI.schemeIs("about")
) {
return false;
}
return true;
}
_isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
if (!this.mBlank || !aWebProgress.isTopLevel) {
return false;
}
// If the state has STATE_STOP, and no requests were in flight, then this
// must be the initial "stop" for the initial about:blank document.
if (
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
this.mRequestCount == 0 &&
!aLocation
) {
return true;
}
let location = aLocation ? aLocation.spec : "";
return location == "about:blank";
}
onProgressChange(
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
) {
this.mTotalProgress = aMaxTotalProgress
? aCurTotalProgress / aMaxTotalProgress
: 0;
if (!this._shouldShowProgress(aRequest)) {
return;
}
if (this.mTotalProgress && this.mTab.hasAttribute("busy")) {
this.mTab.setAttribute("progress", "true");
gBrowser._tabAttrModified(this.mTab, ["progress"]);
}
this._callProgressListeners("onProgressChange", [
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress,
]);
}
onProgressChange64(
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
) {
return this.onProgressChange(
aWebProgress,
aRequest,
aCurSelfProgress,
aMaxSelfProgress,
aCurTotalProgress,
aMaxTotalProgress
);
}
/* eslint-disable complexity */
onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
if (!aRequest) {
return;
}
let location, originalLocation;
try {
aRequest.QueryInterface(Ci.nsIChannel);
location = aRequest.URI;
originalLocation = aRequest.originalURI;
} catch (ex) {}
let ignoreBlank = this._isForInitialAboutBlank(
aWebProgress,
aStateFlags,
location
);
const {
STATE_START,
STATE_STOP,
STATE_IS_NETWORK,
} = Ci.nsIWebProgressListener;
// If we were ignoring some messages about the initial about:blank, and we
// got the STATE_STOP for it, we'll want to pay attention to those messages
// from here forward. Similarly, if we conclude that this state change
// is one that we shouldn't be ignoring, then stop ignoring.
if (
(ignoreBlank &&
aStateFlags & STATE_STOP &&
aStateFlags & STATE_IS_NETWORK) ||
(!ignoreBlank && this.mBlank)
) {
this.mBlank = false;
}
if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) {
this.mRequestCount++;
if (aWebProgress.isTopLevel) {
// Need to use originalLocation rather than location because things
// like about:home and about:privatebrowsing arrive with nsIRequest
// pointing to their resolved jar: or file: URIs.
if (
!(
originalLocation &&
gInitialPages.includes(originalLocation.spec) &&
originalLocation != "about:blank" &&
this.mBrowser.initialPageLoadedFromUserAction !=
originalLocation.spec &&
this.mBrowser.currentURI &&
this.mBrowser.currentURI.spec == "about:blank"
)
) {
// Indicating that we started a load will allow the location
// bar to be cleared when the load finishes.
// In order to not overwrite user-typed content, we avoid it
// (see if condition above) in a very specific case:
// If the load is of an 'initial' page (e.g. about:privatebrowsing,
// about:newtab, etc.), was not explicitly typed in the location
// bar by the user, is not about:blank (because about:blank can be
// loaded by websites under their principal), and the current
// page in the browser is about:blank (indicating it is a newly
// created or re-created browser, e.g. because it just switched
// remoteness or is a new tab/window).
this.mBrowser.urlbarChangeTracker.startedLoad();
}
delete this.mBrowser.initialPageLoadedFromUserAction;
// If the browser is loading it must not be crashed anymore
this.mTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(this.mTab);
}
if (this._shouldShowProgress(aRequest)) {
if (
!(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) &&
aWebProgress &&
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
gBrowser.syncThrobberAnimations(this.mTab);
}
if (this.mTab.selected) {
gBrowser._isBusy = true;
}
}
} else if (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) {
// since we (try to) only handle STATE_STOP of the last request,
// the count of open requests should now be 0
this.mRequestCount = 0;
let modifiedAttrs = [];
if (this.mTab.hasAttribute("busy")) {
this.mTab.removeAttribute("busy");
modifiedAttrs.push("busy");
// Only animate the "burst" indicating the page has loaded if
// the top-level page is the one that finished loading.
if (
aWebProgress.isTopLevel &&
!aWebProgress.isLoadingDocument &&
Components.isSuccessCode(aStatus) &&
!gBrowser.tabAnimationsInProgress &&
!gReduceMotion
) {
if (this.mTab._notselectedsinceload) {
this.mTab.setAttribute("notselectedsinceload", "true");
} else {
this.mTab.removeAttribute("notselectedsinceload");
}
this.mTab.setAttribute("bursting", "true");
}
}
if (this.mTab.hasAttribute("progress")) {
this.mTab.removeAttribute("progress");
modifiedAttrs.push("progress");
}
if (modifiedAttrs.length) {
gBrowser._tabAttrModified(this.mTab, modifiedAttrs);
}
if (aWebProgress.isTopLevel) {
let isSuccessful = Components.isSuccessCode(aStatus);
if (!isSuccessful && !this.mTab.isEmpty) {
// Restore the current document's location in case the
// request was stopped (possibly from a content script)
// before the location changed.
this.mBrowser.userTypedValue = null;
// When browser.tabs.documentchannel.parent-controlled pref and SHIP
// are enabled and a load gets cancelled due to another one
// starting, the error is NS_BINDING_CANCELLED_OLD_LOAD.
// When these prefs are not enabled, the error is different and
// that's why we still want to look at the isNavigating flag.
// We could add a workaround and make sure that in the alternative
// codepaths we would also omit the same error, but considering
// how we will be enabling fission by default soon, we can keep
// using isNavigating for now, and remove it when the
// parent-controlled pref and SHIP are enabled by default.
// Bug 1725716 has been filed to consider removing isNavigating
// field alltogether.
let isNavigating = this.mBrowser.isNavigating;
if (
this.mTab.selected &&
aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD &&
!isNavigating
) {
gURLBar.setURI();
}
} else if (isSuccessful) {
this.mBrowser.urlbarChangeTracker.finishedLoad();
}
}
// If we don't already have an icon for this tab then clear the tab's
// icon. Don't do this on the initial about:blank load to prevent
// flickering. Don't clear the icon if we already set it from one of the
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
if (
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS)
) {
this.mTab.removeAttribute("image");
}
// For keyword URIs clear the user typed value since they will be changed into real URIs
if (location.scheme == "keyword") {
this.mBrowser.userTypedValue = null;
}
if (this.mTab.selected) {
gBrowser._isBusy = false;
}
}
if (ignoreBlank) {
this._callProgressListeners(
"onUpdateCurrentBrowser",
[aStateFlags, aStatus, "", 0],
true,
false
);
} else {
this._callProgressListeners(
"onStateChange",
[aWebProgress, aRequest, aStateFlags, aStatus],
true,
false
);
}
this._callProgressListeners(
"onStateChange",
[aWebProgress, aRequest, aStateFlags, aStatus],
false
);
if (aStateFlags & (STATE_START | STATE_STOP)) {
// reset cached temporary values at beginning and end
this.mMessage = "";
this.mTotalProgress = 0;
}
this.mStateFlags = aStateFlags;
this.mStatus = aStatus;
}
/* eslint-enable complexity */
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
// OnLocationChange is called for both the top-level content
// and the subframes.
let topLevel = aWebProgress.isTopLevel;
let isSameDocument = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
);
if (topLevel) {
let isReload = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD
);
let isErrorPage = !!(
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE
);
// We need to clear the typed value
// if the document failed to load, to make sure the urlbar reflects the
// failed URI (particularly for SSL errors). However, don't clear the value
// if the error page's URI is about:blank, because that causes complete
// loss of urlbar contents for invalid URI errors (see bug 867957).
// Another reason to clear the userTypedValue is if this was an anchor
// navigation initiated by the user.
// Finally, we do insert the URL if this is a same-document navigation
// and the user cleared the URL manually.
if (
this.mBrowser.didStartLoadSinceLastUserTyping() ||
(isErrorPage && aLocation.spec != "about:blank") ||
(isSameDocument && this.mBrowser.isNavigating) ||
(isSameDocument && !this.mBrowser.userTypedValue)
) {
this.mBrowser.userTypedValue = null;
}
// If the tab has been set to "busy" outside the stateChange
// handler below (e.g. by sessionStore.navigateAndRestore), and
// the load results in an error page, it's possible that there
// isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
// attribute being removed. In this case we should remove the
// attribute here.
if (isErrorPage && this.mTab.hasAttribute("busy")) {
this.mTab.removeAttribute("busy");
gBrowser._tabAttrModified(this.mTab, ["busy"]);
}
if (!isSameDocument) {
// If the browser was playing audio, we should remove the playing state.
if (this.mTab.hasAttribute("soundplaying")) {
clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
this.mTab._soundPlayingAttrRemovalTimer = 0;
this.mTab.removeAttribute("soundplaying");
gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
}
// If the browser was previously muted, we should restore the muted state.
if (this.mTab.hasAttribute("muted")) {
this.mTab.linkedBrowser.mute();
}
if (gBrowser.isFindBarInitialized(this.mTab)) {
let findBar = gBrowser.getCachedFindBar(this.mTab);
// Close the Find toolbar if we're in old-style TAF mode
if (findBar.findMode != findBar.FIND_NORMAL) {
findBar.close();
}
}
// Note that we're not updating for same-document loads, despite
// the `title` argument to `history.pushState/replaceState`. For
// context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653
// and https://github.com/whatwg/html/issues/2174
if (!isReload) {
gBrowser.setTabTitle(this.mTab);
}
// Don't clear the favicon if this tab is in the pending
// state, as SessionStore will have set the icon for us even
// though we're pointed at an about:blank. Also don't clear it
// if the tab is in customize mode, to keep the one set by
// gCustomizeMode.setTab (bug 1551239). Also don't clear it
// if onLocationChange was triggered by a pushState or a
// replaceState (bug 550565) or a hash change (bug 408415).
if (
!this.mTab.hasAttribute("pending") &&
!this.mTab.hasAttribute("customizemode") &&
aWebProgress.isLoadingDocument
) {
// Removing the tab's image here causes flickering, wait until the
// load is complete.
this.mBrowser.mIconURL = null;
}
if (
aRequest instanceof Ci.nsIChannel &&
!isBlankPageURL(aRequest.originalURI.spec)
) {
this.mBrowser.originalURI = aRequest.originalURI;
}
}
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
if (this.mBrowser.registeredOpenURI) {
let uri = this.mBrowser.registeredOpenURI;
gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab(
uri.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
);
delete this.mBrowser.registeredOpenURI;
}
if (!isBlankPageURL(aLocation.spec)) {
gBrowser.UrlbarProviderOpenTabs.registerOpenTab(
aLocation.spec,
userContextId,
PrivateBrowsingUtils.isWindowPrivate(window)
);
this.mBrowser.registeredOpenURI = aLocation;
}
if (this.mTab != gBrowser.selectedTab) {
let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab);
if (tabCacheIndex != -1) {
gBrowser._tabLayerCache.splice(tabCacheIndex, 1);
gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab);
}
} else {
if (
gBrowser.featureCallout &&
(gBrowser.featureCalloutPanelId !==
gBrowser.selectedTab.linkedPanel ||
gBrowser.contentPrincipal.originNoSuffix !== "resource://pdf.js")
) {
gBrowser.featureCallout.endTour(true);
gBrowser.featureCallout = null;
}
// For now, only check for Feature Callout messages
// when viewing PDFs. Later, we can expand this to check
// for callout messages on every change of tab location.
if (
!gBrowser.featureCallout &&
gBrowser.contentPrincipal.originNoSuffix === "resource://pdf.js"
) {
gBrowser.instantiateFeatureCalloutTour(
gBrowser.selectedBrowser,
gBrowser.selectedTab.linkedPanel
);
gBrowser.featureCallout.showFeatureCallout();
}
}
}
if (!this.mBlank || this.mBrowser.hasContentOpener) {
this._callProgressListeners("onLocationChange", [
aWebProgress,
aRequest,
aLocation,
aFlags,
]);
if (topLevel && !isSameDocument) {
// Include the true final argument to indicate that this event is
// simulated (instead of being observed by the webProgressListener).
this._callProgressListeners("onContentBlockingEvent", [
aWebProgress,
null,
0,
true,
]);
}
}
if (topLevel) {
this.mBrowser.lastURI = aLocation;
this.mBrowser.lastLocationChange = Date.now();
}
}
onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
if (this.mBlank) {
return;
}
this._callProgressListeners("onStatusChange", [
aWebProgress,
aRequest,
aStatus,
aMessage,
]);
this.mMessage = aMessage;
}
onSecurityChange(aWebProgress, aRequest, aState) {
this._callProgressListeners("onSecurityChange", [
aWebProgress,
aRequest,
aState,
]);
}
onContentBlockingEvent(aWebProgress, aRequest, aEvent) {
this._callProgressListeners("onContentBlockingEvent", [
aWebProgress,
aRequest,
aEvent,
]);
}
onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
return this._callProgressListeners("onRefreshAttempted", [
aWebProgress,
aURI,
aDelay,
aSameURI,
]);
}
}
TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsIWebProgressListener2",
"nsISupportsWeakReference",
]);
} // end private scope for gBrowser
var StatusPanel = {
get panel() {
delete this.panel;
this.panel = document.getElementById("statuspanel");
this.panel.addEventListener(
"transitionend",
this._onTransitionEnd.bind(this)
);
this.panel.addEventListener(
"transitioncancel",
this._onTransitionEnd.bind(this)
);
return this.panel;
},
get isVisible() {
return !this.panel.hasAttribute("inactive");
},
update() {
if (BrowserHandler.kiosk) {
return;
}
let text;
let type;
let types = ["overLink"];
if (XULBrowserWindow.busyUI) {
types.push("status");
}
types.push("defaultStatus");
for (type of types) {
if ((text = XULBrowserWindow[type])) {
break;
}
}
// If it's a long data: URI that uses base64 encoding, truncate to
// a reasonable length rather than trying to display the entire thing.
// We can't shorten arbitrary URIs like this, as bidi etc might mean
// we need the trailing characters for display. But a base64-encoded
// data-URI is plain ASCII, so this is OK for status panel display.
// (See bug 1484071.)
let textCropped = false;
if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) {
text = text.substring(0, 500) + "\u2026";
textCropped = true;
}
if (this._labelElement.value != text || (text && !this.isVisible)) {
this.panel.setAttribute("previoustype", this.panel.getAttribute("type"));
this.panel.setAttribute("type", type);
this._label = text;
this._labelElement.setAttribute(
"crop",
type == "overLink" && !textCropped ? "center" : "end"
);
}
},
get _labelElement() {
delete this._labelElement;
return (this._labelElement = document.getElementById("statuspanel-label"));
},
set _label(val) {
if (!this.isVisible) {
this.panel.removeAttribute("mirror");
this.panel.removeAttribute("sizelimit");
}
if (
this.panel.getAttribute("type") == "status" &&
this.panel.getAttribute("previoustype") == "status"
) {
// Before updating the label, set the panel's current width as its
// min-width to let the panel grow but not shrink and prevent
// unnecessary flicker while loading pages. We only care about the
// panel's width once it has been painted, so we can do this
// without flushing layout.
this.panel.style.minWidth =
window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px";
} else {
this.panel.style.minWidth = "";
}
if (val) {
this._labelElement.value = val;
if (this.panel.hidden) {
this.panel.hidden = false;
// This ensures that the "inactive" attribute removal triggers a
// transition.
getComputedStyle(this.panel).display;
}
this.panel.removeAttribute("inactive");
MousePosTracker.addListener(this);
} else {
this.panel.setAttribute("inactive", "true");
MousePosTracker.removeListener(this);
}
},
_onTransitionEnd() {
if (!this.isVisible) {
this.panel.hidden = true;
}
},
getMouseTargetRect() {
let container = this.panel.parentNode;
let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel);
let containerRect = window.windowUtils.getBoundsWithoutFlushing(container);
return {
top: panelRect.top,
bottom: panelRect.bottom,
left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left,
right: RTL_UI
? containerRect.right
: containerRect.left + panelRect.width,
};
},
onMouseEnter() {
this._mirror();
},
onMouseLeave() {
this._mirror();
},
_mirror() {
if (this.panel.hasAttribute("mirror")) {
this.panel.removeAttribute("mirror");
} else {
this.panel.setAttribute("mirror", "true");
}
if (!this.panel.hasAttribute("sizelimit")) {
this.panel.setAttribute("sizelimit", "true");
}
},
};
var TabBarVisibility = {
_initialUpdateDone: false,
update() {
let toolbar = document.getElementById("TabsToolbar");
let collapse = false;
if (
!gBrowser /* gBrowser isn't initialized yet */ ||
gBrowser.visibleTabs.length == 1
) {
collapse = !window.toolbar.visible;
}
if (collapse == toolbar.collapsed && this._initialUpdateDone) {
return;
}
this._initialUpdateDone = true;
toolbar.collapsed = collapse;
let navbar = document.getElementById("nav-bar");
navbar.setAttribute("tabs-hidden", collapse);
document.getElementById("menu_closeWindow").hidden = collapse;
document.l10n.setAttributes(
document.getElementById("menu_close"),
collapse ? "tabbrowser-menuitem-close" : "tabbrowser-menuitem-close-tab"
);
TabsInTitlebar.allowedBy("tabs-visible", !collapse);
},
};
var TabContextMenu = {
contextTab: null,
_updateToggleMuteMenuItems(aTab, aConditionFn) {
["muted", "soundplaying"].forEach(attr => {
if (!aConditionFn || aConditionFn(attr)) {
if (aTab.hasAttribute(attr)) {
aTab.toggleMuteMenuItem.setAttribute(attr, "true");
aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
} else {
aTab.toggleMuteMenuItem.removeAttribute(attr);
aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
}
}
});
},
updateContextMenu(aPopupMenu) {
let tab =
aPopupMenu.triggerNode &&
(aPopupMenu.triggerNode.tab || aPopupMenu.triggerNode.closest("tab"));
this.contextTab = tab || gBrowser.selectedTab;
this.contextTab.addEventListener("TabAttrModified", this);
aPopupMenu.addEventListener("popuphiding", this);
let disabled = gBrowser.tabs.length == 1;
let multiselectionContext = this.contextTab.multiselected;
let tabCountInfo = JSON.stringify({
tabCount: (multiselectionContext && gBrowser.multiSelectedTabsCount) || 1,
});
var menuItems = aPopupMenu.getElementsByAttribute(
"tbattr",
"tabbrowser-multiple"
);
for (let menuItem of menuItems) {
menuItem.disabled = disabled;
}
disabled = gBrowser.visibleTabs.length == 1;
menuItems = aPopupMenu.getElementsByAttribute(
"tbattr",
"tabbrowser-multiple-visible"
);
for (let menuItem of menuItems) {
menuItem.disabled = disabled;
}
// Session store
document.getElementById("context_undoCloseTab").disabled =
SessionStore.getClosedTabCount(window) == 0;
// Show/hide fullscreen context menu items and set the
// autohide item's checked state to mirror the autohide pref.
showFullScreenViewContextMenuItems(aPopupMenu);
// Only one of Reload_Tab/Reload_Selected_Tabs should be visible.
document.getElementById("context_reloadTab").hidden = multiselectionContext;
document.getElementById(
"context_reloadSelectedTabs"
).hidden = !multiselectionContext;
// Show Play Tab menu item if the tab has attribute activemedia-blocked
document.getElementById("context_playTab").hidden = !(
this.contextTab.activeMediaBlocked && !multiselectionContext
);
document.getElementById("context_playSelectedTabs").hidden = !(
this.contextTab.activeMediaBlocked && multiselectionContext
);
// Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible
let contextPinTab = document.getElementById("context_pinTab");
contextPinTab.hidden = this.contextTab.pinned || multiselectionContext;
let contextUnpinTab = document.getElementById("context_unpinTab");
contextUnpinTab.hidden = !this.contextTab.pinned || multiselectionContext;
let contextPinSelectedTabs = document.getElementById(
"context_pinSelectedTabs"
);
contextPinSelectedTabs.hidden =
this.contextTab.pinned || !multiselectionContext;
let contextUnpinSelectedTabs = document.getElementById(
"context_unpinSelectedTabs"
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !multiselectionContext;
// Move Tab items
let contextMoveTabOptions = document.getElementById(
"context_moveTabOptions"
);
contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo);
contextMoveTabOptions.disabled =
this.contextTab.hidden || gBrowser.allTabsSelected();
let selectedTabs = gBrowser.selectedTabs;
let contextMoveTabToEnd = document.getElementById("context_moveToEnd");
let allSelectedTabsAdjacent = selectedTabs.every(
(element, index, array) => {
return array.length > index + 1
? element._tPos + 1 == array[index + 1]._tPos
: true;
}
);
let contextTabIsSelected = this.contextTab.multiselected;
let visibleTabs = gBrowser.visibleTabs;
let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
let tabsToMove = contextTabIsSelected ? selectedTabs : [this.contextTab];
let lastTabToMove = tabsToMove[tabsToMove.length - 1];
let isLastPinnedTab = false;
if (lastTabToMove.pinned) {
let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
isLastPinnedTab = !sibling || !sibling.pinned;
}
contextMoveTabToEnd.disabled =
(lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
allSelectedTabsAdjacent;
let contextMoveTabToStart = document.getElementById("context_moveToStart");
let isFirstTab =
tabsToMove[0] == visibleTabs[0] ||
tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
if (this.contextTab.hasAttribute("customizemode")) {
document.getElementById("context_openTabInWindow").disabled = true;
}
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
document.getElementById(
"context_duplicateTab"
).hidden = multiselectionContext;
document.getElementById(
"context_duplicateTabs"
).hidden = !multiselectionContext;
// Disable "Close Tabs to the Left/Right" if there are no tabs
// preceding/following it.
let closeTabsToTheStartItem = document.getElementById(
"context_closeTabsToTheStart"
);
let noTabsToStart = !gBrowser.getTabsToTheStartFrom(this.contextTab).length;
closeTabsToTheStartItem.disabled = noTabsToStart;
let closeTabsToTheEndItem = document.getElementById(
"context_closeTabsToTheEnd"
);
let noTabsToEnd = !gBrowser.getTabsToTheEndFrom(this.contextTab).length;
closeTabsToTheEndItem.disabled = noTabsToEnd;
// Disable "Close other Tabs" if there are no unpinned tabs.
let unpinnedTabsToClose = multiselectionContext
? gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length
: gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned)
.length;
let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;
// Update the close item with how many tabs will close.
document
.getElementById("context_closeTab")
.setAttribute("data-l10n-args", tabCountInfo);
// Disable "Close Multiple Tabs" if all sub menuitems are disabled
document.getElementById("context_closeTabOptions").disabled =
closeTabsToTheStartItem.disabled &&
closeTabsToTheEndItem.disabled &&
closeOtherTabsItem.disabled;
// Hide "Bookmark Tab…" for multiselection.
// Update its state if visible.
let bookmarkTab = document.getElementById("context_bookmarkTab");
bookmarkTab.hidden = multiselectionContext;
// Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise.
let bookmarkMultiSelectedTabs = document.getElementById(
"context_bookmarkSelectedTabs"
);
bookmarkMultiSelectedTabs.hidden = !multiselectionContext;
let toggleMute = document.getElementById("context_toggleMuteTab");
let toggleMultiSelectMute = document.getElementById(
"context_toggleMuteSelectedTabs"
);
// Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
toggleMute.hidden = multiselectionContext;
toggleMultiSelectMute.hidden = !multiselectionContext;
const isMuted = this.contextTab.hasAttribute("muted");
document.l10n.setAttributes(
toggleMute,
isMuted ? "tabbrowser-context-unmute-tab" : "tabbrowser-context-mute-tab"
);
document.l10n.setAttributes(
toggleMultiSelectMute,
isMuted
? "tabbrowser-context-unmute-selected-tabs"
: "tabbrowser-context-mute-selected-tabs"
);
this.contextTab.toggleMuteMenuItem = toggleMute;
this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
this._updateToggleMuteMenuItems(this.contextTab);
let selectAllTabs = document.getElementById("context_selectAllTabs");
selectAllTabs.disabled = gBrowser.allTabsSelected();
gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
let reopenInContainer = document.getElementById(
"context_reopenInContainer"
);
reopenInContainer.hidden =
!Services.prefs.getBoolPref("privacy.userContext.enabled", false) ||
PrivateBrowsingUtils.isWindowPrivate(window);
reopenInContainer.disabled = this.contextTab.hidden;
gShareUtils.updateShareURLMenuItem(
this.contextTab.linkedBrowser,
document.getElementById("context_sendTabToDevice")
);
},
handleEvent(aEvent) {
switch (aEvent.type) {
case "popuphiding":
if (aEvent.target.id == "tabContextMenu") {
this.contextTab.removeEventListener("TabAttrModified", this);
}
break;
case "TabAttrModified":
let tab = aEvent.target;
this._updateToggleMuteMenuItems(tab, attr =>
aEvent.detail.changed.includes(attr)
);
break;
}
},
createReopenInContainerMenu(event) {
createUserContextMenu(event, {
isContextMenu: true,
excludeUserContextId: this.contextTab.getAttribute("usercontextid"),
});
},
duplicateSelectedTabs() {
let tabsToDuplicate = gBrowser.selectedTabs;
let newIndex = tabsToDuplicate[tabsToDuplicate.length - 1]._tPos + 1;
for (let tab of tabsToDuplicate) {
let newTab = SessionStore.duplicateTab(window, tab);
gBrowser.moveTabTo(newTab, newIndex++);
}
},
reopenInContainer(event) {
let userContextId = parseInt(
event.target.getAttribute("data-usercontextid")
);
let reopenedTabs = this.contextTab.multiselected
? gBrowser.selectedTabs
: [this.contextTab];
for (let tab of reopenedTabs) {
if (tab.getAttribute("usercontextid") == userContextId) {
continue;
}
/* Create a triggering principal that is able to load the new tab
For content principals that are about: chrome: or resource: we need system to load them.
Anything other than system principal needs to have the new userContextId.
*/
let triggeringPrincipal;
if (tab.linkedPanel) {
triggeringPrincipal = tab.linkedBrowser.contentPrincipal;
} else {
// For lazy tab browsers, get the original principal
// from SessionStore
let tabState = JSON.parse(SessionStore.getTabState(tab));
try {
triggeringPrincipal = E10SUtils.deserializePrincipal(
tabState.triggeringPrincipal_base64
);
} catch (ex) {
continue;
}
}
if (!triggeringPrincipal || triggeringPrincipal.isNullPrincipal) {
// Ensure that we have a null principal if we couldn't
// deserialize it (for lazy tab browsers) ...
// This won't always work however is safe to use.
triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal(
{ userContextId }
);
} else if (triggeringPrincipal.isContentPrincipal) {
triggeringPrincipal = Services.scriptSecurityManager.principalWithOA(
triggeringPrincipal,
{
userContextId,
}
);
}
let newTab = gBrowser.addTab(tab.linkedBrowser.currentURI.spec, {
userContextId,
pinned: tab.pinned,
index: tab._tPos + 1,
triggeringPrincipal,
});
if (gBrowser.selectedTab == tab) {
gBrowser.selectedTab = newTab;
}
if (tab.muted && !newTab.muted) {
newTab.toggleMuteAudio(tab.muteReason);
}
}
},
closeContextTabs(event) {
if (this.contextTab.multiselected) {
gBrowser.removeMultiSelectedTabs();
} else {
gBrowser.removeTab(this.contextTab, { animate: true });
}
},
};