/* -*- 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, }, _visibleTabs: null, _tabs: null, _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() { if (!this._tabs) { this._tabs = this.tabContainer.allTabs; } return this._tabs; }, 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() { if (!this._visibleTabs) { this._visibleTabs = Array.prototype.filter.call( this.tabs, tab => !tab.hidden && !tab.closing ); } return this._visibleTabs; }, 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(location, 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, prefName: "browser.pdfjs.feature-tour", source: location.spec, }); }, _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; }, _invalidateCachedTabs() { this._tabs = null; this._visibleTabs = null; }, _invalidateCachedVisibleTabs() { this._visibleTabs = null; }, _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.currentURI.spec.endsWith(".pdf") ) { this._instantiateFeatureCalloutTour( newBrowser.currentURI, 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 . // 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 }); }, _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 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._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._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._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._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} * 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._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._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._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._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._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 ? [indexOfTab1, indexOfTab2] : [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 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 || !aLocation.spec.endsWith(".pdf")) ) { 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 && aLocation.spec.endsWith(".pdf")) { gBrowser.instantiateFeatureCalloutTour( aLocation, 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 }); } }, };