diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/base/content/browser.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | browser/base/content/browser.js | 10159 |
1 files changed, 10159 insertions, 0 deletions
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js new file mode 100644 index 0000000000..4f145c4949 --- /dev/null +++ b/browser/base/content/browser.js @@ -0,0 +1,10159 @@ +/* -*- 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/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +ChromeUtils.importESModule("resource://gre/modules/NotificationDB.sys.mjs"); + +// lazy module getters + +ChromeUtils.defineESModuleGetters(this, { + AMTelemetry: "resource://gre/modules/AddonManager.sys.mjs", + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + AboutReaderParent: "resource:///actors/AboutReaderParent.sys.mjs", + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs", + BrowserTelemetryUtils: "resource://gre/modules/BrowserTelemetryUtils.sys.mjs", + BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", + BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + CFRPageActions: "resource:///modules/asrouter/CFRPageActions.sys.mjs", + Color: "resource://gre/modules/Color.sys.mjs", + ContentAnalysis: "resource:///modules/ContentAnalysis.sys.mjs", + ContextualIdentityService: + "resource://gre/modules/ContextualIdentityService.sys.mjs", + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + Deprecated: "resource://gre/modules/Deprecated.sys.mjs", + DevToolsSocketStatus: + "resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs", + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", + DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs", + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs", + FirefoxViewNotificationManager: + "resource:///modules/firefox-view-notification-manager.sys.mjs", + HomePage: "resource:///modules/HomePage.sys.mjs", + isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs", + LightweightThemeConsumer: + "resource://gre/modules/LightweightThemeConsumer.sys.mjs", + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + LoginManagerParent: "resource://gre/modules/LoginManagerParent.sys.mjs", + MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + NewTabPagePreloading: "resource:///modules/NewTabPagePreloading.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.sys.mjs", + PageActions: "resource:///modules/PageActions.sys.mjs", + PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", + PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", + PanelView: "resource:///modules/PanelMultiView.sys.mjs", + PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs", + PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs", + PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + Pocket: "chrome://pocket/content/Pocket.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.sys.mjs", + PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", + ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs", + ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs", + ReportBrokenSite: "resource:///modules/ReportBrokenSite.sys.mjs", + SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs", + Sanitizer: "resource:///modules/Sanitizer.sys.mjs", + SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs", + ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", + SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs", + SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + ShoppingSidebarParent: "resource:///actors/ShoppingSidebarParent.sys.mjs", + ShoppingSidebarManager: "resource:///actors/ShoppingSidebarParent.sys.mjs", + ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", + SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", + SitePermissions: "resource:///modules/SitePermissions.sys.mjs", + SubDialog: "resource://gre/modules/SubDialog.sys.mjs", + SubDialogManager: "resource://gre/modules/SubDialog.sys.mjs", + TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs", + TabModalPrompt: "chrome://global/content/tabprompts.sys.mjs", + TabsSetupFlowManager: + "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs", + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", + TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", + UITour: "resource:///modules/UITour.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", + URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs", + UrlbarInput: "resource:///modules/UrlbarInput.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.sys.mjs", + Weave: "resource://services-sync/main.sys.mjs", + WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs", + webrtcUI: "resource:///modules/webrtcUI.sys.mjs", + WebsiteFilter: "resource:///modules/policies/WebsiteFilter.sys.mjs", + ZoomUI: "resource:///modules/ZoomUI.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); + +XPCOMUtils.defineLazyScriptGetter( + this, + "PlacesTreeView", + "chrome://browser/content/places/treeView.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"], + "chrome://browser/content/places/controller.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "PrintUtils", + "chrome://global/content/printUtils.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "ZoomManager", + "chrome://global/content/viewZoomOverlay.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "FullZoom", + "chrome://browser/content/browser-fullZoom.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "PanelUI", + "chrome://browser/content/customizableui/panelUI.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gViewSourceUtils", + "chrome://global/content/viewSourceUtils.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gTabsPanel", + "chrome://browser/content/browser-allTabsMenu.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + [ + "BrowserAddonUI", + "gExtensionsNotifications", + "gUnifiedExtensions", + "gXPInstallObserver", + ], + "chrome://browser/content/browser-addons.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "ctrlTab", + "chrome://browser/content/browser-ctrlTab.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["CustomizationHandler", "AutoHideMenubar"], + "chrome://browser/content/browser-customization.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["PointerLock", "FullScreen"], + "chrome://browser/content/browser-fullScreenAndPointerLock.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gIdentityHandler", + "chrome://browser/content/browser-siteIdentity.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gPermissionPanel", + "chrome://browser/content/browser-sitePermissionPanel.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "TranslationsPanel", + "chrome://browser/content/translations/translationsPanel.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gProtectionsHandler", + "chrome://browser/content/browser-siteProtections.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["gGestureSupport", "gHistorySwipeAnimation"], + "chrome://browser/content/browser-gestureSupport.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gSafeBrowsing", + "chrome://browser/content/browser-safebrowsing.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gSync", + "chrome://browser/content/browser-sync.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gBrowserThumbnails", + "chrome://browser/content/browser-thumbnails.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["openContextMenu", "nsContextMenu"], + "chrome://browser/content/nsContextMenu.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + [ + "DownloadsPanel", + "DownloadsOverlayLoader", + "DownloadsView", + "DownloadsViewUI", + "DownloadsViewController", + "DownloadsSummary", + "DownloadsFooter", + "DownloadsBlockedSubview", + ], + "chrome://browser/content/downloads/downloads.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["DownloadsButton", "DownloadsIndicatorView"], + "chrome://browser/content/downloads/indicator.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gEditItemOverlay", + "chrome://browser/content/places/editBookmark.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gGfxUtils", + "chrome://browser/content/browser-graphics-utils.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "pktUI", + "chrome://pocket/content/pktUI.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "ToolbarKeyboardNavigator", + "chrome://browser/content/browser-toolbarKeyNav.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "A11yUtils", + "chrome://browser/content/browser-a11yUtils.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gSharedTabWarning", + "chrome://browser/content/browser-webrtc.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + "gPageStyleMenu", + "chrome://browser/content/browser-pagestyle.js" +); + +// lazy service getters + +XPCOMUtils.defineLazyServiceGetters(this, { + ContentPrefService2: [ + "@mozilla.org/content-pref/service;1", + "nsIContentPrefService2", + ], + classifierService: [ + "@mozilla.org/url-classifier/dbservice;1", + "nsIURIClassifier", + ], + Favicons: ["@mozilla.org/browser/favicon-service;1", "nsIFaviconService"], + WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"], + BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"], +}); + +if (AppConstants.ENABLE_WEBDRIVER) { + XPCOMUtils.defineLazyServiceGetter( + this, + "Marionette", + "@mozilla.org/remote/marionette;1", + "nsIMarionette" + ); + + XPCOMUtils.defineLazyServiceGetter( + this, + "RemoteAgent", + "@mozilla.org/remote/agent;1", + "nsIRemoteAgent" + ); +} else { + this.Marionette = { running: false }; + this.RemoteAgent = { running: false }; +} + +ChromeUtils.defineLazyGetter(this, "RTL_UI", () => { + return Services.locale.isAppLocaleRTL; +}); + +ChromeUtils.defineLazyGetter(this, "gBrandBundle", () => { + return Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); +}); + +ChromeUtils.defineLazyGetter(this, "gBrowserBundle", () => { + return Services.strings.createBundle( + "chrome://browser/locale/browser.properties" + ); +}); + +ChromeUtils.defineLazyGetter(this, "gCustomizeMode", () => { + let { CustomizeMode } = ChromeUtils.importESModule( + "resource:///modules/CustomizeMode.sys.mjs" + ); + return new CustomizeMode(window); +}); + +ChromeUtils.defineLazyGetter(this, "gNavToolbox", () => { + return document.getElementById("navigator-toolbox"); +}); + +ChromeUtils.defineLazyGetter(this, "gURLBar", () => { + let urlbar = new UrlbarInput({ + textbox: document.getElementById("urlbar"), + eventTelemetryCategory: "urlbar", + }); + + let beforeFocusOrSelect = event => { + // In customize mode, the url bar is disabled. If a new tab is opened or the + // user switches to a different tab, this function gets called before we've + // finished leaving customize mode, and the url bar will still be disabled. + // We can't focus it when it's disabled, so we need to re-run ourselves when + // we've finished leaving customize mode. + if ( + CustomizationHandler.isCustomizing() || + CustomizationHandler.isExitingCustomizeMode + ) { + gNavToolbox.addEventListener( + "aftercustomization", + () => { + if (event.type == "beforeselect") { + gURLBar.select(); + } else { + gURLBar.focus(); + } + }, + { + once: true, + } + ); + event.preventDefault(); + return; + } + + if (window.fullScreen) { + FullScreen.showNavToolbox(); + } + }; + urlbar.addEventListener("beforefocus", beforeFocusOrSelect); + urlbar.addEventListener("beforeselect", beforeFocusOrSelect); + + return urlbar; +}); + +ChromeUtils.defineLazyGetter(this, "ReferrerInfo", () => + Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ) +); + +// High priority notification bars shown at the top of the window. +ChromeUtils.defineLazyGetter(this, "gNotificationBox", () => { + return new MozElements.NotificationBox(element => { + element.classList.add("global-notificationbox"); + element.setAttribute("notificationside", "top"); + element.setAttribute("prepend-notifications", true); + const tabNotifications = document.getElementById("tab-notification-deck"); + gNavToolbox.insertBefore(element, tabNotifications); + }); +}); + +ChromeUtils.defineLazyGetter(this, "InlineSpellCheckerUI", () => { + let { InlineSpellChecker } = ChromeUtils.importESModule( + "resource://gre/modules/InlineSpellChecker.sys.mjs" + ); + return new InlineSpellChecker(); +}); + +ChromeUtils.defineLazyGetter(this, "PopupNotifications", () => { + // eslint-disable-next-line no-shadow + let { PopupNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/PopupNotifications.sys.mjs" + ); + try { + // Hide all PopupNotifications while the the address bar has focus, + // including the virtual focus in the results popup, and the URL is being + // edited or the page proxy state is invalid while async tab switching. + let shouldSuppress = () => { + // "Blank" pages, like about:welcome, have a pageproxystate of "invalid", but + // popups like CFRs should not automatically be suppressed when the address + // bar has focus on these pages as it disrupts user navigation using FN+F6. + // See `UrlbarInput.setURI()` where pageproxystate is set to "invalid" for + // all pages that the "isBlankPageURL" method returns true for. + const urlBarEdited = isBlankPageURL(gBrowser.currentURI.spec) + ? gURLBar.hasAttribute("usertyping") + : gURLBar.getAttribute("pageproxystate") != "valid"; + return ( + (urlBarEdited && gURLBar.focused) || + (gURLBar.getAttribute("pageproxystate") != "valid" && + gBrowser.selectedBrowser._awaitingSetURI) || + shouldSuppressPopupNotifications() + ); + }; + + // Before a Popup is shown, check that its anchor is visible. + // If the anchor is not visible, use one of the fallbacks. + // If no fallbacks are visible, return null. + const getVisibleAnchorElement = anchorElement => { + // If the anchor element is present in the Urlbar, + // ensure that both the anchor and page URL are visible. + gURLBar.maybeHandleRevertFromPopup(anchorElement); + if (anchorElement?.checkVisibility()) { + return anchorElement; + } + let fallback = [ + document.getElementById("identity-icon"), + document.getElementById("urlbar-search-button"), + ]; + return fallback.find(element => element?.checkVisibility()) ?? null; + }; + + return new PopupNotifications( + gBrowser, + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box"), + { shouldSuppress, getVisibleAnchorElement } + ); + } catch (ex) { + console.error(ex); + return null; + } +}); + +ChromeUtils.defineLazyGetter(this, "MacUserActivityUpdater", () => { + if (AppConstants.platform != "macosx") { + return null; + } + + return Cc["@mozilla.org/widget/macuseractivityupdater;1"].getService( + Ci.nsIMacUserActivityUpdater + ); +}); + +ChromeUtils.defineLazyGetter(this, "Win7Features", () => { + if (AppConstants.platform != "win") { + return null; + } + + const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + if ( + WINTASKBAR_CONTRACTID in Cc && + Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available + ) { + let { AeroPeek } = ChromeUtils.importESModule( + "resource:///modules/WindowsPreviewPerTab.sys.mjs" + ); + return { + onOpenWindow() { + AeroPeek.onOpenWindow(window); + this.handledOpening = true; + }, + onCloseWindow() { + if (this.handledOpening) { + AeroPeek.onCloseWindow(window); + } + }, + handledOpening: false, + }; + } + return null; +}); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gToolbarKeyNavEnabled", + "browser.toolbars.keyboard_navigation", + false, + (aPref, aOldVal, aNewVal) => { + if (window.closed) { + return; + } + if (aNewVal) { + ToolbarKeyboardNavigator.init(); + } else { + ToolbarKeyboardNavigator.uninit(); + } + } +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gBookmarksToolbarVisibility", + "browser.toolbars.bookmarks.visibility", + "newtab" +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gBookmarksToolbarShowInPrivate", + "browser.toolbars.bookmarks.showInPrivateBrowsing", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gFxaToolbarEnabled", + "identity.fxaccounts.toolbar.enabled", + false, + (aPref, aOldVal, aNewVal) => { + updateFxaToolbarMenu(aNewVal); + } +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gFxaToolbarAccessed", + "identity.fxaccounts.toolbar.accessed", + false, + (aPref, aOldVal, aNewVal) => { + updateFxaToolbarMenu(gFxaToolbarEnabled); + } +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gAddonAbuseReportEnabled", + "extensions.abuseReport.enabled", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gAlwaysOpenPanel", + "browser.download.alwaysOpenPanel", + true +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gMiddleClickNewTabUsesPasteboard", + "browser.tabs.searchclipboardfor.middleclick", + true +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gScreenshotsDisabled", + "extensions.screenshots.disabled", + false, + () => { + Services.obs.notifyObservers( + window, + "toggle-screenshot-disable", + gScreenshots.shouldScreenshotsButtonBeDisabled() + ); + } +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gPrintEnabled", + "print.enabled", + false, + (aPref, aOldVal, aNewVal) => { + updatePrintCommands(aNewVal); + } +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gScreenshotsComponentEnabled", + "screenshots.browser.component.enabled", + false, + () => { + Services.obs.notifyObservers( + window, + "toggle-screenshot-disable", + gScreenshots.shouldScreenshotsButtonBeDisabled() + ); + } +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gTranslationsEnabled", + "browser.translations.enable", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "gUseFeltPrivacyUI", + "browser.privatebrowsing.felt-privacy-v1", + false +); + +customElements.setElementCreationCallback("screenshots-buttons", () => { + Services.scriptloader.loadSubScript( + "chrome://browser/content/screenshots/screenshots-buttons.js", + window + ); +}); + +var gBrowser; +var gContextMenu = null; // nsContextMenu instance +var gMultiProcessBrowser = window.docShell.QueryInterface( + Ci.nsILoadContext +).useRemoteTabs; +var gFissionBrowser = window.docShell.QueryInterface( + Ci.nsILoadContext +).useRemoteSubframes; + +var gBrowserAllowScriptsToCloseInitialTabs = false; + +if (AppConstants.platform != "macosx") { + var gEditUIVisible = true; +} + +Object.defineProperty(this, "gReduceMotion", { + enumerable: true, + get() { + return typeof gReduceMotionOverride == "boolean" + ? gReduceMotionOverride + : gReduceMotionSetting; + }, +}); +// Reduce motion during startup. The setting will be reset later. +let gReduceMotionSetting = true; +// This is for tests to set. +var gReduceMotionOverride; + +// Smart getter for the findbar. If you don't wish to force the creation of +// the findbar, check gFindBarInitialized first. + +Object.defineProperty(this, "gFindBar", { + enumerable: true, + get() { + return gBrowser.getCachedFindBar(); + }, +}); + +Object.defineProperty(this, "gFindBarInitialized", { + enumerable: true, + get() { + return gBrowser.isFindBarInitialized(); + }, +}); + +Object.defineProperty(this, "gFindBarPromise", { + enumerable: true, + get() { + return gBrowser.getFindBar(); + }, +}); + +function shouldSuppressPopupNotifications() { + // We have to hide notifications explicitly when the window is + // minimized because of the effects of the "noautohide" attribute on Linux. + // This can be removed once bug 545265 and bug 1320361 are fixed. + // Hide popup notifications when system tab prompts are shown so they + // don't cover up the prompt. + return ( + window.windowState == window.STATE_MINIMIZED || + gBrowser?.selectedBrowser.hasAttribute("tabmodalChromePromptShowing") || + gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing") || + gDialogBox?.isOpen + ); +} + +async function gLazyFindCommand(cmd, ...args) { + let fb = await gFindBarPromise; + // We could be closed by now, or the tab with XBL binding could have gone away: + if (fb && fb[cmd]) { + fb[cmd].apply(fb, args); + } +} + +var gPageIcons = { + "about:home": "chrome://branding/content/icon32.png", + "about:newtab": "chrome://branding/content/icon32.png", + "about:welcome": "chrome://branding/content/icon32.png", + "about:privatebrowsing": "chrome://browser/skin/privatebrowsing/favicon.svg", +}; + +var gInitialPages = [ + "about:blank", + "about:home", + "about:firefoxview", + "about:newtab", + "about:privatebrowsing", + "about:sessionrestore", + "about:welcome", + "about:welcomeback", + "chrome://browser/content/blanktab.html", +]; + +function isInitialPage(url) { + if (!(url instanceof Ci.nsIURI)) { + try { + url = Services.io.newURI(url); + } catch (ex) { + return false; + } + } + + let nonQuery = url.prePath + url.filePath; + return gInitialPages.includes(nonQuery) || nonQuery == BROWSER_NEW_TAB_URL; +} + +function browserWindows() { + return Services.wm.getEnumerator("navigator:browser"); +} + +function updateBookmarkToolbarVisibility() { + // Bug 1846583 - hide bookmarks toolbar in PBM + if ( + gUseFeltPrivacyUI && + !gBookmarksToolbarShowInPrivate && + PrivateBrowsingUtils.isWindowPrivate(window) + ) { + setToolbarVisibility(BookmarkingUI.toolbar, false, false, false); + } else { + BookmarkingUI.updateEmptyToolbarMessage(); + setToolbarVisibility( + BookmarkingUI.toolbar, + gBookmarksToolbarVisibility, + false, + false + ); + } +} + +// This is a stringbundle-like interface to gBrowserBundle, formerly a getter for +// the "bundle_browser" element. +var gNavigatorBundle = { + getString(key) { + return gBrowserBundle.GetStringFromName(key); + }, + getFormattedString(key, array) { + return gBrowserBundle.formatStringFromName(key, array); + }, +}; + +var gScreenshots = { + shouldScreenshotsButtonBeDisabled() { + // About pages other than about:reader are not currently supported by + // the screenshots extension (see Bug 1620992). + let uri = gBrowser.selectedBrowser.currentURI; + let shouldBeDisabled = + gScreenshotsDisabled || + (!gScreenshotsComponentEnabled && + uri.scheme === "about" && + !uri.spec.startsWith("about:reader")); + + return shouldBeDisabled; + }, +}; + +function updateFxaToolbarMenu(enable, isInitialUpdate = false) { + // We only show the Firefox Account toolbar menu if the feature is enabled and + // if sync is enabled. + const syncEnabled = Services.prefs.getBoolPref( + "identity.fxaccounts.enabled", + false + ); + + const mainWindowEl = document.documentElement; + const fxaPanelEl = PanelMultiView.getViewNode(document, "PanelUI-fxa"); + + // To minimize the toolbar button flickering or appearing/disappearing during startup, + // we use this pref to anticipate the likely FxA status. + const statusGuess = !!Services.prefs.getStringPref( + "identity.fxaccounts.account.device.name", + "" + ); + mainWindowEl.setAttribute( + "fxastatus", + statusGuess ? "signed_in" : "not_configured" + ); + + fxaPanelEl.addEventListener("ViewShowing", gSync.updateSendToDeviceTitle); + + Services.telemetry.setEventRecordingEnabled("fxa_app_menu", true); + + if (enable && syncEnabled) { + mainWindowEl.setAttribute("fxatoolbarmenu", "visible"); + + // We have to manually update the sync state UI when toggling the FxA toolbar + // because it could show an invalid icon if the user is logged in and no sync + // event was performed yet. + if (!isInitialUpdate) { + gSync.maybeUpdateUIState(); + } + + Services.telemetry.setEventRecordingEnabled("fxa_avatar_menu", true); + } else { + mainWindowEl.removeAttribute("fxatoolbarmenu"); + } +} + +function UpdateBackForwardCommands(aWebNavigation) { + var backCommand = document.getElementById("Browser:Back"); + var forwardCommand = document.getElementById("Browser:Forward"); + + // Avoid setting attributes on commands if the value hasn't changed! + // Remember, guys, setting attributes on elements is expensive! They + // get inherited into anonymous content, broadcast to other widgets, etc.! + // Don't do it if the value hasn't changed! - dwh + + var backDisabled = backCommand.hasAttribute("disabled"); + var forwardDisabled = forwardCommand.hasAttribute("disabled"); + if (backDisabled == aWebNavigation.canGoBack) { + if (backDisabled) { + backCommand.removeAttribute("disabled"); + } else { + backCommand.setAttribute("disabled", true); + } + } + + if (forwardDisabled == aWebNavigation.canGoForward) { + if (forwardDisabled) { + forwardCommand.removeAttribute("disabled"); + } else { + forwardCommand.setAttribute("disabled", true); + } + } +} + +function updatePrintCommands(enabled) { + var printCommand = document.getElementById("cmd_print"); + var printPreviewCommand = document.getElementById("cmd_printPreviewToggle"); + + if (enabled) { + printCommand.removeAttribute("disabled"); + printPreviewCommand.removeAttribute("disabled"); + } else { + printCommand.setAttribute("disabled", "true"); + printPreviewCommand.setAttribute("disabled", "true"); + } +} + +/** + * Click-and-Hold implementation for the Back and Forward buttons + * XXXmano: should this live in toolbarbutton.js? + */ +function SetClickAndHoldHandlers() { + // Bug 414797: Clone the back/forward buttons' context menu into both buttons. + let popup = document.getElementById("backForwardMenu").cloneNode(true); + popup.removeAttribute("id"); + // Prevent the back/forward buttons' context attributes from being inherited. + popup.setAttribute("context", ""); + + let backButton = document.getElementById("back-button"); + backButton.setAttribute("type", "menu"); + backButton.prepend(popup); + gClickAndHoldListenersOnElement.add(backButton); + + let forwardButton = document.getElementById("forward-button"); + popup = popup.cloneNode(true); + forwardButton.setAttribute("type", "menu"); + forwardButton.prepend(popup); + gClickAndHoldListenersOnElement.add(forwardButton); +} + +const gClickAndHoldListenersOnElement = { + _timers: new Map(), + + _mousedownHandler(aEvent) { + if ( + aEvent.button != 0 || + aEvent.currentTarget.open || + aEvent.currentTarget.disabled + ) { + return; + } + + // Prevent the menupopup from opening immediately + aEvent.currentTarget.menupopup.hidden = true; + + aEvent.currentTarget.addEventListener("mouseout", this); + aEvent.currentTarget.addEventListener("mouseup", this); + this._timers.set( + aEvent.currentTarget, + setTimeout(b => this._openMenu(b), 500, aEvent.currentTarget) + ); + }, + + _clickHandler(aEvent) { + if ( + aEvent.button == 0 && + aEvent.target == aEvent.currentTarget && + !aEvent.currentTarget.open && + !aEvent.currentTarget.disabled && + // When menupopup is not hidden and we receive + // a click event, it means the mousedown occurred + // on aEvent.currentTarget and mouseup occurred on + // aEvent.currentTarget.menupopup, we don't + // need to handle the click event as menupopup + // handled mouseup event already. + aEvent.currentTarget.menupopup.hidden + ) { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + window, + 0, + aEvent.ctrlKey, + aEvent.altKey, + aEvent.shiftKey, + aEvent.metaKey, + 0, + null, + aEvent.inputSource + ); + aEvent.currentTarget.dispatchEvent(cmdEvent); + + // This is here to cancel the XUL default event + // dom.click() triggers a command even if there is a click handler + // however this can now be prevented with preventDefault(). + aEvent.preventDefault(); + } + }, + + _openMenu(aButton) { + this._cancelHold(aButton); + aButton.firstElementChild.hidden = false; + aButton.open = true; + }, + + _mouseoutHandler(aEvent) { + let buttonRect = aEvent.currentTarget.getBoundingClientRect(); + if ( + aEvent.clientX >= buttonRect.left && + aEvent.clientX <= buttonRect.right && + aEvent.clientY >= buttonRect.bottom + ) { + this._openMenu(aEvent.currentTarget); + } else { + this._cancelHold(aEvent.currentTarget); + } + }, + + _mouseupHandler(aEvent) { + this._cancelHold(aEvent.currentTarget); + }, + + _cancelHold(aButton) { + clearTimeout(this._timers.get(aButton)); + aButton.removeEventListener("mouseout", this); + aButton.removeEventListener("mouseup", this); + }, + + _keypressHandler(aEvent) { + if (aEvent.key == " " || aEvent.key == "Enter") { + // Normally, command events get fired for keyboard activation. However, + // we've set type="menu", so that doesn't happen. Handle this the same + // way we handle clicks. + aEvent.target.click(); + } + }, + + handleEvent(e) { + switch (e.type) { + case "mouseout": + this._mouseoutHandler(e); + break; + case "mousedown": + this._mousedownHandler(e); + break; + case "click": + this._clickHandler(e); + break; + case "mouseup": + this._mouseupHandler(e); + break; + case "keypress": + this._keypressHandler(e); + break; + } + }, + + remove(aButton) { + aButton.removeEventListener("mousedown", this, true); + aButton.removeEventListener("click", this, true); + aButton.removeEventListener("keypress", this, true); + }, + + add(aElm) { + this._timers.delete(aElm); + + aElm.addEventListener("mousedown", this, true); + aElm.addEventListener("click", this, true); + aElm.addEventListener("keypress", this, true); + }, +}; + +const gSessionHistoryObserver = { + observe(subject, topic, data) { + if (topic != "browser:purge-session-history") { + return; + } + + var backCommand = document.getElementById("Browser:Back"); + backCommand.setAttribute("disabled", "true"); + var fwdCommand = document.getElementById("Browser:Forward"); + fwdCommand.setAttribute("disabled", "true"); + + // Clear undo history of the URL bar + gURLBar.editor.clearUndoRedo(); + }, +}; + +const gStoragePressureObserver = { + _lastNotificationTime: -1, + + async observe(subject, topic, data) { + if (topic != "QuotaManager::StoragePressure") { + return; + } + + const NOTIFICATION_VALUE = "storage-pressure-notification"; + if (gNotificationBox.getNotificationWithValue(NOTIFICATION_VALUE)) { + // Do not display the 2nd notification when there is already one + return; + } + + // Don't display notification twice within the given interval. + // This is because + // - not to annoy user + // - give user some time to clean space. + // Even user sees notification and starts acting, it still takes some time. + const MIN_NOTIFICATION_INTERVAL_MS = Services.prefs.getIntPref( + "browser.storageManager.pressureNotification.minIntervalMS" + ); + let duration = Date.now() - this._lastNotificationTime; + if (duration <= MIN_NOTIFICATION_INTERVAL_MS) { + return; + } + this._lastNotificationTime = Date.now(); + + MozXULElement.insertFTLIfNeeded("branding/brand.ftl"); + MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl"); + + const BYTES_IN_GIGABYTE = 1073741824; + const USAGE_THRESHOLD_BYTES = + BYTES_IN_GIGABYTE * + Services.prefs.getIntPref( + "browser.storageManager.pressureNotification.usageThresholdGB" + ); + let messageFragment = document.createDocumentFragment(); + let message = document.createElement("span"); + + let buttons = [{ supportPage: "storage-permissions" }]; + let usage = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (usage < USAGE_THRESHOLD_BYTES) { + // The firefox-used space < 5GB, then warn user to free some disk space. + // This is because this usage is small and not the main cause for space issue. + // In order to avoid the bad and wrong impression among users that + // firefox eats disk space a lot, indicate users to clean up other disk space. + document.l10n.setAttributes(message, "space-alert-under-5gb-message2"); + } else { + // The firefox-used space >= 5GB, then guide users to about:preferences + // to clear some data stored on firefox by websites. + document.l10n.setAttributes(message, "space-alert-over-5gb-message2"); + buttons.push({ + "l10n-id": "space-alert-over-5gb-settings-button", + callback(notificationBar, button) { + // The advanced subpanes are only supported in the old organization, which will + // be removed by bug 1349689. + openPreferences("privacy-sitedata"); + }, + }); + } + messageFragment.appendChild(message); + + await gNotificationBox.appendNotification( + NOTIFICATION_VALUE, + { + label: messageFragment, + priority: gNotificationBox.PRIORITY_WARNING_HIGH, + }, + buttons + ); + + // This seems to be necessary to get the buttons to display correctly + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1504216 + document.l10n.translateFragment(gNotificationBox.currentNotification); + }, +}; + +var gPopupBlockerObserver = { + async handleEvent(aEvent) { + if (aEvent.originalTarget != gBrowser.selectedBrowser) { + return; + } + + gPermissionPanel.refreshPermissionIcons(); + + let popupCount = + gBrowser.selectedBrowser.popupBlocker.getBlockedPopupCount(); + + if (!popupCount) { + // Hide the notification box (if it's visible). + let notificationBox = gBrowser.getNotificationBox(); + let notification = + notificationBox.getNotificationWithValue("popup-blocked"); + if (notification) { + notificationBox.removeNotification(notification, false); + } + return; + } + + // Only show the notification again if we've not already shown it. Since + // notifications are per-browser, we don't need to worry about re-adding + // it. + if (gBrowser.selectedBrowser.popupBlocker.shouldShowNotification) { + if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) { + const label = { + "l10n-id": + popupCount < this.maxReportedPopups + ? "popup-warning-message" + : "popup-warning-exceeded-message", + "l10n-args": { popupCount }, + }; + + let notificationBox = gBrowser.getNotificationBox(); + let notification = + notificationBox.getNotificationWithValue("popup-blocked") || + (await this.notificationPromise); + if (notification) { + notification.label = label; + } else { + const image = "chrome://browser/skin/notification-icons/popup.svg"; + const priority = notificationBox.PRIORITY_INFO_MEDIUM; + try { + this.notificationPromise = notificationBox.appendNotification( + "popup-blocked", + { label, image, priority }, + [ + { + "l10n-id": "popup-warning-button", + popup: "blockedPopupOptions", + callback: null, + }, + ] + ); + await this.notificationPromise; + } catch (err) { + console.warn(err); + } finally { + this.notificationPromise = null; + } + } + } + + // Record the fact that we've reported this blocked popup, so we don't + // show it again. + gBrowser.selectedBrowser.popupBlocker.didShowNotification(); + } + }, + + toggleAllowPopupsForSite(aEvent) { + var pm = Services.perms; + var shouldBlock = aEvent.target.getAttribute("block") == "true"; + var perm = shouldBlock ? pm.DENY_ACTION : pm.ALLOW_ACTION; + pm.addFromPrincipal(gBrowser.contentPrincipal, "popup", perm); + + if (!shouldBlock) { + gBrowser.selectedBrowser.popupBlocker.unblockAllPopups(); + } + + gBrowser.getNotificationBox().removeCurrentNotification(); + }, + + fillPopupList(aEvent) { + // XXXben - rather than using |currentURI| here, which breaks down on multi-framed sites + // we should really walk the blockedPopups and create a list of "allow for <host>" + // menuitems for the common subset of hosts present in the report, this will + // make us frame-safe. + // + // XXXjst - Note that when this is fixed to work with multi-framed sites, + // also back out the fix for bug 343772 where + // nsGlobalWindow::CheckOpenAllow() was changed to also + // check if the top window's location is allow-listed. + let browser = gBrowser.selectedBrowser; + var uriOrPrincipal = browser.contentPrincipal.isContentPrincipal + ? browser.contentPrincipal + : browser.currentURI; + var blockedPopupAllowSite = document.getElementById( + "blockedPopupAllowSite" + ); + try { + blockedPopupAllowSite.removeAttribute("hidden"); + let uriHost = uriOrPrincipal.asciiHost + ? uriOrPrincipal.host + : uriOrPrincipal.spec; + var pm = Services.perms; + if ( + pm.testPermissionFromPrincipal(browser.contentPrincipal, "popup") == + pm.ALLOW_ACTION + ) { + // Offer an item to block popups for this site, if an allow-list entry exists + // already for it. + document.l10n.setAttributes( + blockedPopupAllowSite, + "popups-infobar-block", + { uriHost } + ); + blockedPopupAllowSite.setAttribute("block", "true"); + } else { + // Offer an item to allow popups for this site + document.l10n.setAttributes( + blockedPopupAllowSite, + "popups-infobar-allow", + { uriHost } + ); + blockedPopupAllowSite.removeAttribute("block"); + } + } catch (e) { + blockedPopupAllowSite.hidden = true; + } + + let blockedPopupDontShowMessage = document.getElementById( + "blockedPopupDontShowMessage" + ); + let showMessage = Services.prefs.getBoolPref( + "privacy.popups.showBrowserMessage" + ); + blockedPopupDontShowMessage.setAttribute("checked", !showMessage); + + let blockedPopupsSeparator = document.getElementById( + "blockedPopupsSeparator" + ); + blockedPopupsSeparator.hidden = true; + + browser.popupBlocker.getBlockedPopups().then(blockedPopups => { + let foundUsablePopupURI = false; + if (blockedPopups) { + for (let i = 0; i < blockedPopups.length; i++) { + let blockedPopup = blockedPopups[i]; + + // popupWindowURI will be null if the file picker popup is blocked. + // xxxdz this should make the option say "Show file picker" and do it (Bug 590306) + if (!blockedPopup.popupWindowURISpec) { + continue; + } + + var popupURIspec = blockedPopup.popupWindowURISpec; + + // Sometimes the popup URI that we get back from the blockedPopup + // isn't useful (for instance, netscape.com's popup URI ends up + // being "http://www.netscape.com", which isn't really the URI of + // the popup they're trying to show). This isn't going to be + // useful to the user, so we won't create a menu item for it. + if ( + popupURIspec == "" || + popupURIspec == "about:blank" || + popupURIspec == "<self>" || + popupURIspec == uriOrPrincipal.spec + ) { + continue; + } + + // Because of the short-circuit above, we may end up in a situation + // in which we don't have any usable popup addresses to show in + // the menu, and therefore we shouldn't show the separator. However, + // since we got past the short-circuit, we must've found at least + // one usable popup URI and thus we'll turn on the separator later. + foundUsablePopupURI = true; + + var menuitem = document.createXULElement("menuitem"); + document.l10n.setAttributes(menuitem, "popup-show-popup-menuitem", { + popupURI: popupURIspec, + }); + menuitem.setAttribute( + "oncommand", + "gPopupBlockerObserver.showBlockedPopup(event);" + ); + menuitem.setAttribute("popupReportIndex", i); + menuitem.setAttribute( + "popupInnerWindowId", + blockedPopup.innerWindowId + ); + menuitem.browsingContext = blockedPopup.browsingContext; + menuitem.popupReportBrowser = browser; + aEvent.target.appendChild(menuitem); + } + } + + // Show the separator if we added any + // showable popup addresses to the menu. + if (foundUsablePopupURI) { + blockedPopupsSeparator.removeAttribute("hidden"); + } + }, null); + }, + + onPopupHiding(aEvent) { + let item = aEvent.target.lastElementChild; + while (item && item.id != "blockedPopupsSeparator") { + let next = item.previousElementSibling; + item.remove(); + item = next; + } + }, + + showBlockedPopup(aEvent) { + let target = aEvent.target; + let browsingContext = target.browsingContext; + let innerWindowId = target.getAttribute("popupInnerWindowId"); + let popupReportIndex = target.getAttribute("popupReportIndex"); + let browser = target.popupReportBrowser; + browser.popupBlocker.unblockPopup( + browsingContext, + innerWindowId, + popupReportIndex + ); + }, + + editPopupSettings() { + openPreferences("privacy-permissions-block-popups"); + }, + + dontShowMessage() { + var showMessage = Services.prefs.getBoolPref( + "privacy.popups.showBrowserMessage" + ); + Services.prefs.setBoolPref( + "privacy.popups.showBrowserMessage", + !showMessage + ); + gBrowser.getNotificationBox().removeCurrentNotification(); + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter( + gPopupBlockerObserver, + "maxReportedPopups", + "privacy.popups.maxReported" +); + +var gKeywordURIFixup = { + check(browser, { fixedURI, keywordProviderName, preferredURI }) { + // We get called irrespective of whether we did a keyword search, or + // whether the original input would be vaguely interpretable as a URL, + // so figure that out first. + if ( + !keywordProviderName || + !fixedURI || + !fixedURI.host || + UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") || + UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") == 0 + ) { + return; + } + + let contentPrincipal = browser.contentPrincipal; + + // At this point we're still only just about to load this URI. + // When the async DNS lookup comes back, we may be in any of these states: + // 1) still on the previous URI, waiting for the preferredURI (keyword + // search) to respond; + // 2) at the keyword search URI (preferredURI) + // 3) at some other page because the user stopped navigation. + // We keep track of the currentURI to detect case (1) in the DNS lookup + // callback. + let previousURI = browser.currentURI; + + // now swap for a weak ref so we don't hang on to browser needlessly + // even if the DNS query takes forever + let weakBrowser = Cu.getWeakReference(browser); + browser = null; + + // Additionally, we need the host of the parsed url + let hostName = fixedURI.displayHost; + // and the ascii-only host for the pref: + let asciiHost = fixedURI.asciiHost; + + let onLookupCompleteListener = { + async onLookupComplete(request, record, status) { + let browserRef = weakBrowser.get(); + if (!Components.isSuccessCode(status) || !browserRef) { + return; + } + + let currentURI = browserRef.currentURI; + // If we're in case (3) (see above), don't show an info bar. + if ( + !currentURI.equals(previousURI) && + !currentURI.equals(preferredURI) + ) { + return; + } + + // show infobar offering to visit the host + let notificationBox = gBrowser.getNotificationBox(browserRef); + if (notificationBox.getNotificationWithValue("keyword-uri-fixup")) { + return; + } + + let displayHostName = "http://" + hostName + "/"; + let message = gNavigatorBundle.getFormattedString( + "keywordURIFixup.message", + [displayHostName] + ); + let yesMessage = gNavigatorBundle.getFormattedString( + "keywordURIFixup.goTo", + [displayHostName] + ); + + let buttons = [ + { + label: yesMessage, + accessKey: gNavigatorBundle.getString( + "keywordURIFixup.goTo.accesskey" + ), + callback() { + // Do not set this preference while in private browsing. + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + let prefHost = asciiHost; + // Normalize out a single trailing dot - NB: not using endsWith/lastIndexOf + // because we need to be sure this last dot is the *only* dot, too. + // More generally, this is used for the pref and should stay in sync with + // the code in URIFixup::KeywordURIFixup . + if (prefHost.indexOf(".") == prefHost.length - 1) { + prefHost = prefHost.slice(0, -1); + } + let pref = "browser.fixup.domainwhitelist." + prefHost; + Services.prefs.setBoolPref(pref, true); + } + openTrustedLinkIn(fixedURI.spec, "current"); + }, + }, + ]; + let notification = await notificationBox.appendNotification( + "keyword-uri-fixup", + { + label: message, + priority: notificationBox.PRIORITY_INFO_HIGH, + }, + buttons + ); + notification.persistence = 1; + }, + }; + + Services.uriFixup.checkHost( + fixedURI, + onLookupCompleteListener, + contentPrincipal.originAttributes + ); + }, + + observe(fixupInfo, topic, data) { + fixupInfo.QueryInterface(Ci.nsIURIFixupInfo); + + let browser = fixupInfo.consumer?.top?.embedderElement; + if (!browser || browser.ownerGlobal != window) { + return; + } + + this.check(browser, fixupInfo); + }, +}; + +/* Creates a null principal using the userContextId + from the current selected tab or a passed in tab argument */ +function _createNullPrincipalFromTabUserContextId(tab = gBrowser.selectedTab) { + let userContextId; + if (tab.hasAttribute("usercontextid")) { + userContextId = tab.getAttribute("usercontextid"); + } + return Services.scriptSecurityManager.createNullPrincipal({ + userContextId, + }); +} + +let _resolveDelayedStartup; +var delayedStartupPromise = new Promise(resolve => { + _resolveDelayedStartup = resolve; +}); + +var gBrowserInit = { + delayedStartupFinished: false, + idleTasksFinishedPromise: null, + idleTaskPromiseResolve: null, + domContentLoaded: false, + + _tabToAdopt: undefined, + + _setupFirstContentWindowPaintPromise() { + let lastTransactionId = window.windowUtils.lastTransactionId; + let layerTreeListener = () => { + if (this.getTabToAdopt()) { + // Need to wait until we finish adopting the tab, or we might end + // up focusing the initial browser and then losing focus when it + // gets swapped out for the tab to adopt. + return; + } + removeEventListener("MozLayerTreeReady", layerTreeListener); + let listener = e => { + if (e.transactionId > lastTransactionId) { + window.removeEventListener("MozAfterPaint", listener); + this._firstContentWindowPaintDeferred.resolve(); + } + }; + addEventListener("MozAfterPaint", listener); + }; + addEventListener("MozLayerTreeReady", layerTreeListener); + }, + + getTabToAdopt() { + if (this._tabToAdopt !== undefined) { + return this._tabToAdopt; + } + + if (window.arguments && window.XULElement.isInstance(window.arguments[0])) { + this._tabToAdopt = window.arguments[0]; + + // Clear the reference of the tab being adopted from the arguments. + window.arguments[0] = null; + } else { + // There was no tab to adopt in the arguments, set _tabToAdopt to null + // to avoid checking it again. + this._tabToAdopt = null; + } + + return this._tabToAdopt; + }, + + _clearTabToAdopt() { + this._tabToAdopt = null; + }, + + // Used to check if the new window is still adopting an existing tab as its first tab + // (e.g. from the WebExtensions internals). + isAdoptingTab() { + return !!this.getTabToAdopt(); + }, + + onBeforeInitialXULLayout() { + this._setupFirstContentWindowPaintPromise(); + + updateBookmarkToolbarVisibility(); + + // Set a sane starting width/height for all resolutions on new profiles. + if (ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)) { + // When the fingerprinting resistance is enabled, making sure that we don't + // have a maximum window to interfere with generating rounded window dimensions. + document.documentElement.setAttribute("sizemode", "normal"); + } else if (!document.documentElement.hasAttribute("width")) { + const TARGET_WIDTH = 1280; + const TARGET_HEIGHT = 1040; + let width = Math.min(screen.availWidth * 0.9, TARGET_WIDTH); + let height = Math.min(screen.availHeight * 0.9, TARGET_HEIGHT); + + document.documentElement.setAttribute("width", width); + document.documentElement.setAttribute("height", height); + + if (width < TARGET_WIDTH && height < TARGET_HEIGHT) { + document.documentElement.setAttribute("sizemode", "maximized"); + } + } + if (AppConstants.MENUBAR_CAN_AUTOHIDE) { + const toolbarMenubar = document.getElementById("toolbar-menubar"); + // set a default value + if (!toolbarMenubar.hasAttribute("autohide")) { + toolbarMenubar.setAttribute("autohide", true); + } + document.l10n.setAttributes( + toolbarMenubar, + "toolbar-context-menu-menu-bar-cmd" + ); + toolbarMenubar.setAttribute("data-l10n-attrs", "toolbarname"); + } + + // Run menubar initialization first, to avoid TabsInTitlebar code picking + // up mutations from it and causing a reflow. + AutoHideMenubar.init(); + // Update the chromemargin attribute so the window can be sized correctly. + window.TabBarVisibility.update(); + TabsInTitlebar.init(); + + new LightweightThemeConsumer(document); + + if ( + Services.prefs.getBoolPref( + "toolkit.legacyUserProfileCustomizations.windowIcon", + false + ) + ) { + document.documentElement.setAttribute("icon", "main-window"); + } + + // Call this after we set attributes that might change toolbars' computed + // text color. + ToolbarIconColor.init(); + }, + + onDOMContentLoaded() { + // This needs setting up before we create the first remote browser. + window.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow; + window.browserDOMWindow = new nsBrowserAccess(); + + gBrowser = window._gBrowser; + delete window._gBrowser; + gBrowser.init(); + + BrowserWindowTracker.track(window); + + FirefoxViewHandler.init(); + + gNavToolbox.palette = document.getElementById( + "BrowserToolbarPalette" + ).content; + for (let area of CustomizableUI.areas) { + let type = CustomizableUI.getAreaType(area); + if (type == CustomizableUI.TYPE_TOOLBAR) { + let node = document.getElementById(area); + CustomizableUI.registerToolbarNode(node); + } + } + BrowserSearch.initPlaceHolder(); + + // Hack to ensure that the various initial pages favicon is loaded + // instantaneously, to avoid flickering and improve perceived performance. + this._callWithURIToLoad(uriToLoad => { + let url; + try { + url = Services.io.newURI(uriToLoad); + } catch (e) { + return; + } + let nonQuery = url.prePath + url.filePath; + if (nonQuery in gPageIcons) { + gBrowser.setIcon(gBrowser.selectedTab, gPageIcons[nonQuery]); + } + }); + + updateFxaToolbarMenu(gFxaToolbarEnabled, true); + + updatePrintCommands(gPrintEnabled); + + gUnifiedExtensions.init(); + + // Setting the focus will cause a style flush, it's preferable to call anything + // that will modify the DOM from within this function before this call. + this._setInitialFocus(); + + this.domContentLoaded = true; + }, + + onLoad() { + gBrowser.addEventListener("DOMUpdateBlockedPopups", gPopupBlockerObserver); + gBrowser.addEventListener( + "TranslationsParent:LanguageState", + TranslationsPanel + ); + gBrowser.addEventListener( + "TranslationsParent:OfferTranslation", + TranslationsPanel + ); + gBrowser.addTabsProgressListener(TranslationsPanel); + + window.addEventListener("AppCommand", HandleAppCommandEvent, true); + + // These routines add message listeners. They must run before + // loading the frame script to ensure that we don't miss any + // message sent between when the frame script is loaded and when + // the listener is registered. + CaptivePortalWatcher.init(); + ZoomUI.init(window); + + if (!gMultiProcessBrowser) { + // There is a Content:Click message manually sent from content. + Services.els.addSystemEventListener( + gBrowser.tabpanels, + "click", + contentAreaClick, + true + ); + } + + // hook up UI through progress listener + gBrowser.addProgressListener(window.XULBrowserWindow); + gBrowser.addTabsProgressListener(window.TabsProgressListener); + + SidebarUI.init(); + + // We do this in onload because we want to ensure the button's state + // doesn't flicker as the window is being shown. + DownloadsButton.init(); + + // Certain kinds of automigration rely on this notification to complete + // their tasks BEFORE the browser window is shown. SessionStore uses it to + // restore tabs into windows AFTER important parts like gMultiProcessBrowser + // have been initialized. + Services.obs.notifyObservers(window, "browser-window-before-show"); + + if (!window.toolbar.visible) { + // adjust browser UI for popups + gURLBar.readOnly = true; + } + + // Misc. inits. + gUIDensity.init(); + TabletModeUpdater.init(); + CombinedStopReload.ensureInitialized(); + gPrivateBrowsingUI.init(); + BrowserSearch.init(); + BrowserPageActions.init(); + if (gToolbarKeyNavEnabled) { + ToolbarKeyboardNavigator.init(); + } + + // Update UI if browser is under remote control. + gRemoteControl.updateVisualCue(); + + // If we are given a tab to swap in, take care of it before first paint to + // avoid an about:blank flash. + let tabToAdopt = this.getTabToAdopt(); + if (tabToAdopt) { + let evt = new CustomEvent("before-initial-tab-adopted", { + bubbles: true, + }); + gBrowser.tabpanels.dispatchEvent(evt); + + // Stop the about:blank load + gBrowser.stop(); + + // Remove the speculative focus from the urlbar to let the url be formatted. + gURLBar.removeAttribute("focused"); + + let swapBrowsers = () => { + try { + gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToAdopt); + } catch (e) { + console.error(e); + } + + // Clear the reference to the tab once its adoption has been completed. + this._clearTabToAdopt(); + }; + if (tabToAdopt.linkedBrowser.isRemoteBrowser) { + // For remote browsers, wait for the paint event, otherwise the tabs + // are not yet ready and focus gets confused because the browser swaps + // out while tabs are switching. + addEventListener("MozAfterPaint", swapBrowsers, { once: true }); + } else { + swapBrowsers(); + } + } + + // Wait until chrome is painted before executing code not critical to making the window visible + this._boundDelayedStartup = this._delayedStartup.bind(this); + window.addEventListener("MozAfterPaint", this._boundDelayedStartup); + + if (!PrivateBrowsingUtils.enabled) { + document.getElementById("Tools:PrivateBrowsing").hidden = true; + // Setting disabled doesn't disable the shortcut, so we just remove + // the keybinding. + document.getElementById("key_privatebrowsing").remove(); + } + + if (BrowserUIUtils.quitShortcutDisabled) { + document.getElementById("key_quitApplication").remove(); + document.getElementById("menu_FileQuitItem").removeAttribute("key"); + + PanelMultiView.getViewNode( + document, + "appMenu-quit-button2" + )?.removeAttribute("key"); + } + + this._loadHandled = true; + }, + + _cancelDelayedStartup() { + window.removeEventListener("MozAfterPaint", this._boundDelayedStartup); + this._boundDelayedStartup = null; + }, + + _delayedStartup() { + let { TelemetryTimestamps } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryTimestamps.sys.mjs" + ); + TelemetryTimestamps.add("delayedStartupStarted"); + + this._cancelDelayedStartup(); + + // Bug 1531854 - The hidden window is force-created here + // until all of its dependencies are handled. + Services.appShell.hiddenDOMWindow; + + gBrowser.addEventListener( + "PermissionStateChange", + function () { + gIdentityHandler.refreshIdentityBlock(); + gPermissionPanel.updateSharingIndicator(); + }, + true + ); + + this._handleURIToLoad(); + + Services.obs.addObserver(gIdentityHandler, "perm-changed"); + Services.obs.addObserver(gRemoteControl, "devtools-socket"); + Services.obs.addObserver(gRemoteControl, "marionette-listening"); + Services.obs.addObserver(gRemoteControl, "remote-listening"); + Services.obs.addObserver( + gSessionHistoryObserver, + "browser:purge-session-history" + ); + Services.obs.addObserver( + gStoragePressureObserver, + "QuotaManager::StoragePressure" + ); + Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled"); + Services.obs.addObserver(gXPInstallObserver, "addon-install-started"); + Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked"); + Services.obs.addObserver( + gXPInstallObserver, + "addon-install-fullscreen-blocked" + ); + Services.obs.addObserver( + gXPInstallObserver, + "addon-install-origin-blocked" + ); + Services.obs.addObserver( + gXPInstallObserver, + "addon-install-policy-blocked" + ); + Services.obs.addObserver( + gXPInstallObserver, + "addon-install-webapi-blocked" + ); + Services.obs.addObserver(gXPInstallObserver, "addon-install-failed"); + Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation"); + Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup"); + + BrowserOffline.init(); + CanvasPermissionPromptHelper.init(); + WebAuthnPromptHelper.init(); + ContentAnalysis.initialize(); + + // Initialize the full zoom setting. + // We do this before the session restore service gets initialized so we can + // apply full zoom settings to tabs restored by the session restore service. + FullZoom.init(); + PanelUI.init(shouldSuppressPopupNotifications); + ReportBrokenSite.init(gBrowser); + + UpdateUrlbarSearchSplitterState(); + + BookmarkingUI.init(); + BrowserSearch.delayedStartupInit(); + SearchUIUtils.init(); + gProtectionsHandler.init(); + HomePage.delayedStartup().catch(console.error); + + let safeMode = document.getElementById("helpSafeMode"); + if (Services.appinfo.inSafeMode) { + document.l10n.setAttributes(safeMode, "menu-help-exit-troubleshoot-mode"); + safeMode.setAttribute( + "appmenu-data-l10n-id", + "appmenu-help-exit-troubleshoot-mode" + ); + } + + // BiDi UI + gBidiUI = isBidiEnabled(); + if (gBidiUI) { + document.getElementById("documentDirection-separator").hidden = false; + document.getElementById("documentDirection-swap").hidden = false; + document.getElementById("textfieldDirection-separator").hidden = false; + document.getElementById("textfieldDirection-swap").hidden = false; + } + + // Setup click-and-hold gestures access to the session history + // menus if global click-and-hold isn't turned on + if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) { + SetClickAndHoldHandlers(); + } + + function initBackForwardButtonTooltip(tooltipId, l10nId, shortcutId) { + let shortcut = document.getElementById(shortcutId); + shortcut = ShortcutUtils.prettifyShortcut(shortcut); + + let tooltip = document.getElementById(tooltipId); + document.l10n.setAttributes(tooltip, l10nId, { shortcut }); + } + + initBackForwardButtonTooltip( + "back-button-tooltip-description", + "navbar-tooltip-back-2", + "goBackKb" + ); + + initBackForwardButtonTooltip( + "forward-button-tooltip-description", + "navbar-tooltip-forward-2", + "goForwardKb" + ); + + PlacesToolbarHelper.init(); + + ctrlTab.readPref(); + Services.prefs.addObserver(ctrlTab.prefName, ctrlTab); + + // The object handling the downloads indicator is initialized here in the + // delayed startup function, but the actual indicator element is not loaded + // unless there are downloads to be displayed. + DownloadsButton.initializeIndicator(); + + if (AppConstants.platform != "macosx") { + updateEditUIVisibility(); + let placesContext = document.getElementById("placesContext"); + placesContext.addEventListener("popupshowing", updateEditUIVisibility); + placesContext.addEventListener("popuphiding", updateEditUIVisibility); + } + + FullScreen.init(); + MenuTouchModeObserver.init(); + + if (AppConstants.MOZ_DATA_REPORTING) { + gDataNotificationInfoBar.init(); + } + + if (!AppConstants.MOZILLA_OFFICIAL) { + DevelopmentHelpers.init(); + } + + gExtensionsNotifications.init(); + + let wasMinimized = window.windowState == window.STATE_MINIMIZED; + window.addEventListener("sizemodechange", () => { + let isMinimized = window.windowState == window.STATE_MINIMIZED; + if (wasMinimized != isMinimized) { + wasMinimized = isMinimized; + UpdatePopupNotificationsVisibility(); + } + }); + + window.addEventListener("mousemove", MousePosTracker); + window.addEventListener("dragover", MousePosTracker); + + gNavToolbox.addEventListener("customizationstarting", CustomizationHandler); + gNavToolbox.addEventListener("aftercustomization", CustomizationHandler); + + SessionStore.promiseInitialized.then(() => { + // Bail out if the window has been closed in the meantime. + if (window.closed) { + return; + } + + // Enable the Restore Last Session command if needed + RestoreLastSessionObserver.init(); + + SidebarUI.startDelayedLoad(); + + PanicButtonNotifier.init(); + }); + + if (BrowserHandler.kiosk) { + // We don't modify popup windows for kiosk mode + if (!gURLBar.readOnly) { + window.fullScreen = true; + } + } + + if (Services.policies.status === Services.policies.ACTIVE) { + if (!Services.policies.isAllowed("hideShowMenuBar")) { + document + .getElementById("toolbar-menubar") + .removeAttribute("toolbarname"); + } + if (!Services.policies.isAllowed("filepickers")) { + let savePageCommand = document.getElementById("Browser:SavePage"); + let openFileCommand = document.getElementById("Browser:OpenFile"); + + savePageCommand.setAttribute("disabled", "true"); + openFileCommand.setAttribute("disabled", "true"); + + document.addEventListener("FilePickerBlocked", function (event) { + let browser = event.target; + + let notificationBox = browser + .getTabBrowser() + ?.getNotificationBox(browser); + + // Prevent duplicate notifications + if ( + notificationBox && + !notificationBox.getNotificationWithValue("filepicker-blocked") + ) { + notificationBox.appendNotification("filepicker-blocked", { + label: { + "l10n-id": "filepicker-blocked-infobar", + }, + priority: notificationBox.PRIORITY_INFO_LOW, + }); + } + }); + } + let policies = Services.policies.getActivePolicies(); + if ("ManagedBookmarks" in policies) { + let managedBookmarks = policies.ManagedBookmarks; + let children = managedBookmarks.filter( + child => !("toplevel_name" in child) + ); + if (children.length) { + let managedBookmarksButton = + document.createXULElement("toolbarbutton"); + managedBookmarksButton.setAttribute("id", "managed-bookmarks"); + managedBookmarksButton.setAttribute("class", "bookmark-item"); + let toplevel = managedBookmarks.find( + element => "toplevel_name" in element + ); + if (toplevel) { + managedBookmarksButton.setAttribute( + "label", + toplevel.toplevel_name + ); + } else { + document.l10n.setAttributes( + managedBookmarksButton, + "managed-bookmarks" + ); + } + managedBookmarksButton.setAttribute("context", "placesContext"); + managedBookmarksButton.setAttribute("container", "true"); + managedBookmarksButton.setAttribute("removable", "false"); + managedBookmarksButton.setAttribute("type", "menu"); + + let managedBookmarksPopup = document.createXULElement("menupopup"); + managedBookmarksPopup.setAttribute("id", "managed-bookmarks-popup"); + managedBookmarksPopup.setAttribute( + "oncommand", + "PlacesToolbarHelper.openManagedBookmark(event);" + ); + managedBookmarksPopup.setAttribute( + "ondragover", + "event.dataTransfer.effectAllowed='none';" + ); + managedBookmarksPopup.setAttribute( + "ondragstart", + "PlacesToolbarHelper.onDragStartManaged(event);" + ); + managedBookmarksPopup.setAttribute( + "onpopupshowing", + "PlacesToolbarHelper.populateManagedBookmarks(this);" + ); + managedBookmarksPopup.setAttribute("placespopup", "true"); + managedBookmarksPopup.setAttribute("is", "places-popup"); + managedBookmarksPopup.classList.add("toolbar-menupopup"); + managedBookmarksButton.appendChild(managedBookmarksPopup); + + gNavToolbox.palette.appendChild(managedBookmarksButton); + + CustomizableUI.ensureWidgetPlacedInWindow( + "managed-bookmarks", + window + ); + + // Add button if it doesn't exist + if (!CustomizableUI.getPlacementOfWidget("managed-bookmarks")) { + CustomizableUI.addWidgetToArea( + "managed-bookmarks", + CustomizableUI.AREA_BOOKMARKS, + 0 + ); + } + } + } + } + + CaptivePortalWatcher.delayedStartup(); + + ShoppingSidebarManager.ensureInitialized(); + + SessionStore.promiseAllWindowsRestored.then(() => { + this._schedulePerWindowIdleTasks(); + document.documentElement.setAttribute("sessionrestored", "true"); + }); + + this.delayedStartupFinished = true; + _resolveDelayedStartup(); + Services.obs.notifyObservers(window, "browser-delayed-startup-finished"); + TelemetryTimestamps.add("delayedStartupFinished"); + // We've announced that delayed startup has finished. Do not add code past this point. + }, + + /** + * Resolved on the first MozLayerTreeReady and next MozAfterPaint in the + * parent process. + */ + get firstContentWindowPaintPromise() { + return this._firstContentWindowPaintDeferred.promise; + }, + + _setInitialFocus() { + let initiallyFocusedElement = document.commandDispatcher.focusedElement; + + // To prevent startup flicker, the urlbar has the 'focused' attribute set + // by default. If we are not sure the urlbar will be focused in this + // window, we need to remove the attribute before first paint. + // TODO (bug 1629956): The urlbar having the 'focused' attribute by default + // isn't a useful optimization anymore since UrlbarInput needs layout + // information to focus the urlbar properly. + let shouldRemoveFocusedAttribute = true; + + this._callWithURIToLoad(uriToLoad => { + if ( + isBlankPageURL(uriToLoad) || + uriToLoad == "about:privatebrowsing" || + this.getTabToAdopt()?.isEmpty + ) { + gURLBar.select(); + shouldRemoveFocusedAttribute = false; + return; + } + + // If the initial browser is remote, in order to optimize for first paint, + // we'll defer switching focus to that browser until it has painted. + // Otherwise use a regular promise to guarantee that mutationobserver + // microtasks that could affect focusability have run. + let promise = gBrowser.selectedBrowser.isRemoteBrowser + ? this.firstContentWindowPaintPromise + : Promise.resolve(); + + promise.then(() => { + // If focus didn't move while we were waiting, we're okay to move to + // the browser. + if ( + document.commandDispatcher.focusedElement == initiallyFocusedElement + ) { + gBrowser.selectedBrowser.focus(); + } + }); + }); + + // Delay removing the attribute using requestAnimationFrame to avoid + // invalidating styles multiple times in a row if uriToLoadPromise + // resolves before first paint. + if (shouldRemoveFocusedAttribute) { + window.requestAnimationFrame(() => { + if (shouldRemoveFocusedAttribute) { + gURLBar.removeAttribute("focused"); + } + }); + } + }, + + _handleURIToLoad() { + this._callWithURIToLoad(uriToLoad => { + if (!uriToLoad) { + // We don't check whether window.arguments[5] (userContextId) is set + // because tabbrowser.js takes care of that for the initial tab. + return; + } + + // We don't check if uriToLoad is a XULElement because this case has + // already been handled before first paint, and the argument cleared. + if (Array.isArray(uriToLoad)) { + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(uriToLoad, { + inBackground: false, + replace: true, + // See below for the semantics of window.arguments. Only the minimum is supported. + userContextId: window.arguments[5], + triggeringPrincipal: + window.arguments[8] || + Services.scriptSecurityManager.getSystemPrincipal(), + allowInheritPrincipal: window.arguments[9], + csp: window.arguments[10], + fromExternal: true, + }); + } catch (e) {} + } else if (window.arguments.length >= 3) { + // window.arguments[1]: extraOptions (nsIPropertyBag) + // [2]: referrerInfo (nsIReferrerInfo) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + // [5]: userContextId (int) + // [6]: originPrincipal (nsIPrincipal) + // [7]: originStoragePrincipal (nsIPrincipal) + // [8]: triggeringPrincipal (nsIPrincipal) + // [9]: allowInheritPrincipal (bool) + // [10]: csp (nsIContentSecurityPolicy) + // [11]: nsOpenWindowInfo + let userContextId = + window.arguments[5] != undefined + ? window.arguments[5] + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + + let hasValidUserGestureActivation = undefined; + let fromExternal = undefined; + let globalHistoryOptions = undefined; + let triggeringRemoteType = undefined; + let forceAllowDataURI = false; + let wasSchemelessInput = false; + if (window.arguments[1]) { + if (!(window.arguments[1] instanceof Ci.nsIPropertyBag2)) { + throw new Error( + "window.arguments[1] must be null or Ci.nsIPropertyBag2!" + ); + } + + let extraOptions = window.arguments[1]; + if (extraOptions.hasKey("hasValidUserGestureActivation")) { + hasValidUserGestureActivation = extraOptions.getPropertyAsBool( + "hasValidUserGestureActivation" + ); + } + if (extraOptions.hasKey("fromExternal")) { + fromExternal = extraOptions.getPropertyAsBool("fromExternal"); + } + if (extraOptions.hasKey("triggeringSponsoredURL")) { + globalHistoryOptions = { + triggeringSponsoredURL: extraOptions.getPropertyAsACString( + "triggeringSponsoredURL" + ), + }; + if (extraOptions.hasKey("triggeringSponsoredURLVisitTimeMS")) { + globalHistoryOptions.triggeringSponsoredURLVisitTimeMS = + extraOptions.getPropertyAsUint64( + "triggeringSponsoredURLVisitTimeMS" + ); + } + } + if (extraOptions.hasKey("triggeringRemoteType")) { + triggeringRemoteType = extraOptions.getPropertyAsACString( + "triggeringRemoteType" + ); + } + if (extraOptions.hasKey("forceAllowDataURI")) { + forceAllowDataURI = + extraOptions.getPropertyAsBool("forceAllowDataURI"); + } + if (extraOptions.hasKey("wasSchemelessInput")) { + wasSchemelessInput = + extraOptions.getPropertyAsBool("wasSchemelessInput"); + } + } + + try { + openLinkIn(uriToLoad, "current", { + referrerInfo: window.arguments[2] || null, + postData: window.arguments[3] || null, + allowThirdPartyFixup: window.arguments[4] || false, + userContextId, + // pass the origin principal (if any) and force its use to create + // an initial about:blank viewer if present: + originPrincipal: window.arguments[6], + originStoragePrincipal: window.arguments[7], + triggeringPrincipal: window.arguments[8], + // TODO fix allowInheritPrincipal to default to false. + // Default to true unless explicitly set to false because of bug 1475201. + allowInheritPrincipal: window.arguments[9] !== false, + csp: window.arguments[10], + forceAboutBlankViewerInCurrent: !!window.arguments[6], + forceAllowDataURI, + hasValidUserGestureActivation, + fromExternal, + globalHistoryOptions, + triggeringRemoteType, + wasSchemelessInput, + }); + } catch (e) { + console.error(e); + } + + window.focus(); + } else { + // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. + // Such callers expect that window.arguments[0] is handled as a single URI. + loadOneOrMoreURIs( + uriToLoad, + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + } + }); + }, + + /** + * Use this function as an entry point to schedule tasks that + * need to run once per window after startup, and can be scheduled + * by using an idle callback. + * + * The functions scheduled here will fire from idle callbacks + * once every window has finished being restored by session + * restore, and after the equivalent only-once tasks + * have run (from _scheduleStartupIdleTasks in BrowserGlue.sys.mjs). + */ + _schedulePerWindowIdleTasks() { + // Bail out if the window has been closed in the meantime. + if (window.closed) { + return; + } + + function scheduleIdleTask(func, options) { + requestIdleCallback(function idleTaskRunner() { + if (!window.closed) { + func(); + } + }, options); + } + + scheduleIdleTask(() => { + // Initialize the Sync UI + gSync.init(); + }); + + scheduleIdleTask(() => { + // Read prefers-reduced-motion setting + let reduceMotionQuery = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ); + function readSetting() { + gReduceMotionSetting = reduceMotionQuery.matches; + } + reduceMotionQuery.addListener(readSetting); + readSetting(); + }); + + scheduleIdleTask(() => { + // load the tab preview component + import("chrome://browser/content/tabpreview/tabpreview.mjs").catch( + console.error + ); + }); + + scheduleIdleTask(() => { + // setup simple gestures support + gGestureSupport.init(true); + + // setup history swipe animation + gHistorySwipeAnimation.init(); + }); + + scheduleIdleTask(() => { + gBrowserThumbnails.init(); + }); + + scheduleIdleTask( + () => { + // Initialize the download manager some time after the app starts so that + // auto-resume downloads begin (such as after crashing or quitting with + // active downloads) and speeds up the first-load of the download manager UI. + // If the user manually opens the download manager before the timeout, the + // downloads will start right away, and initializing again won't hurt. + try { + DownloadsCommon.initializeAllDataLinks(); + ChromeUtils.importESModule( + "resource:///modules/DownloadsTaskbar.sys.mjs" + ).DownloadsTaskbar.registerIndicator(window); + if (AppConstants.platform == "macosx") { + ChromeUtils.importESModule( + "resource:///modules/DownloadsMacFinderProgress.sys.mjs" + ).DownloadsMacFinderProgress.register(); + } + Services.telemetry.setEventRecordingEnabled("downloads", true); + } catch (ex) { + console.error(ex); + } + }, + { timeout: 10000 } + ); + + if (Win7Features) { + scheduleIdleTask(() => Win7Features.onOpenWindow()); + } + + scheduleIdleTask(async () => { + NewTabPagePreloading.maybeCreatePreloadedBrowser(window); + }); + + scheduleIdleTask(() => { + gGfxUtils.init(); + }); + + // This should always go last, since the idle tasks (except for the ones with + // timeouts) should execute in order. Note that this observer notification is + // not guaranteed to fire, since the window could close before we get here. + scheduleIdleTask(() => { + this.idleTaskPromiseResolve(); + Services.obs.notifyObservers( + window, + "browser-idle-startup-tasks-finished" + ); + }); + }, + + // Returns the URI(s) to load at startup if it is immediately known, or a + // promise resolving to the URI to load. + get uriToLoadPromise() { + delete this.uriToLoadPromise; + return (this.uriToLoadPromise = (function () { + // window.arguments[0]: URI to load (string), or an nsIArray of + // nsISupportsStrings to load, or a xul:tab of + // a tabbrowser, which will be replaced by this + // window (for this case, all other arguments are + // ignored). + let uri = window.arguments?.[0]; + if (!uri || window.XULElement.isInstance(uri)) { + return null; + } + + let defaultArgs = BrowserHandler.defaultArgs; + + // If the given URI is different from the homepage, we want to load it. + if (uri != defaultArgs) { + AboutNewTab.noteNonDefaultStartup(); + + if (uri instanceof Ci.nsIArray) { + // Transform the nsIArray of nsISupportsString's into a JS Array of + // JS strings. + return Array.from( + uri.enumerate(Ci.nsISupportsString), + supportStr => supportStr.data + ); + } else if (uri instanceof Ci.nsISupportsString) { + return uri.data; + } + return uri; + } + + // The URI appears to be the the homepage. We want to load it only if + // session restore isn't about to override the homepage. + let willOverride = SessionStartup.willOverrideHomepage; + if (typeof willOverride == "boolean") { + return willOverride ? null : uri; + } + return willOverride.then(willOverrideHomepage => + willOverrideHomepage ? null : uri + ); + })()); + }, + + // Calls the given callback with the URI to load at startup. + // Synchronously if possible, or after uriToLoadPromise resolves otherwise. + _callWithURIToLoad(callback) { + let uriToLoad = this.uriToLoadPromise; + if (uriToLoad && uriToLoad.then) { + uriToLoad.then(callback); + } else { + callback(uriToLoad); + } + }, + + onUnload() { + gUIDensity.uninit(); + + TabsInTitlebar.uninit(); + + ToolbarIconColor.uninit(); + + // In certain scenarios it's possible for unload to be fired before onload, + // (e.g. if the window is being closed after browser.js loads but before the + // load completes). In that case, there's nothing to do here. + if (!this._loadHandled) { + return; + } + + // First clean up services initialized in gBrowserInit.onLoad (or those whose + // uninit methods don't depend on the services having been initialized). + + CombinedStopReload.uninit(); + + gGestureSupport.init(false); + + gHistorySwipeAnimation.uninit(); + + FullScreen.uninit(); + + gSync.uninit(); + + gExtensionsNotifications.uninit(); + gUnifiedExtensions.uninit(); + + try { + gBrowser.removeProgressListener(window.XULBrowserWindow); + gBrowser.removeTabsProgressListener(window.TabsProgressListener); + } catch (ex) {} + + PlacesToolbarHelper.uninit(); + + BookmarkingUI.uninit(); + + TabletModeUpdater.uninit(); + + gTabletModePageCounter.finish(); + + CaptivePortalWatcher.uninit(); + + SidebarUI.uninit(); + + DownloadsButton.uninit(); + + if (gToolbarKeyNavEnabled) { + ToolbarKeyboardNavigator.uninit(); + } + + BrowserSearch.uninit(); + + NewTabPagePreloading.removePreloadedBrowser(window); + + FirefoxViewHandler.uninit(); + + // Now either cancel delayedStartup, or clean up the services initialized from + // it. + if (this._boundDelayedStartup) { + this._cancelDelayedStartup(); + } else { + if (Win7Features) { + Win7Features.onCloseWindow(); + } + Services.prefs.removeObserver(ctrlTab.prefName, ctrlTab); + ctrlTab.uninit(); + gBrowserThumbnails.uninit(); + gProtectionsHandler.uninit(); + FullZoom.destroy(); + + Services.obs.removeObserver(gIdentityHandler, "perm-changed"); + Services.obs.removeObserver(gRemoteControl, "devtools-socket"); + Services.obs.removeObserver(gRemoteControl, "marionette-listening"); + Services.obs.removeObserver(gRemoteControl, "remote-listening"); + Services.obs.removeObserver( + gSessionHistoryObserver, + "browser:purge-session-history" + ); + Services.obs.removeObserver( + gStoragePressureObserver, + "QuotaManager::StoragePressure" + ); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-started"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked"); + Services.obs.removeObserver( + gXPInstallObserver, + "addon-install-fullscreen-blocked" + ); + Services.obs.removeObserver( + gXPInstallObserver, + "addon-install-origin-blocked" + ); + Services.obs.removeObserver( + gXPInstallObserver, + "addon-install-policy-blocked" + ); + Services.obs.removeObserver( + gXPInstallObserver, + "addon-install-webapi-blocked" + ); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed"); + Services.obs.removeObserver( + gXPInstallObserver, + "addon-install-confirmation" + ); + Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup"); + + MenuTouchModeObserver.uninit(); + BrowserOffline.uninit(); + CanvasPermissionPromptHelper.uninit(); + WebAuthnPromptHelper.uninit(); + PanelUI.uninit(); + } + + // Final window teardown, do this last. + gBrowser.destroy(); + window.XULBrowserWindow = null; + window.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).XULBrowserWindow = null; + window.browserDOMWindow = null; + }, +}; + +ChromeUtils.defineLazyGetter( + gBrowserInit, + "_firstContentWindowPaintDeferred", + () => Promise.withResolvers() +); + +gBrowserInit.idleTasksFinishedPromise = new Promise(resolve => { + gBrowserInit.idleTaskPromiseResolve = resolve; +}); + +function HandleAppCommandEvent(evt) { + switch (evt.command) { + case "Back": + BrowserBack(); + break; + case "Forward": + BrowserForward(); + break; + case "Reload": + BrowserReloadSkipCache(); + break; + case "Stop": + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") { + BrowserStop(); + } + break; + case "Search": + BrowserSearch.webSearch(); + break; + case "Bookmarks": + SidebarUI.toggle("viewBookmarksSidebar"); + break; + case "Home": + BrowserHome(); + break; + case "New": + BrowserOpenTab(); + break; + case "Close": + BrowserCloseTabOrWindow(); + break; + case "Find": + gLazyFindCommand("onFindCommand"); + break; + case "Help": + openHelpLink("firefox-help"); + break; + case "Open": + BrowserOpenFileWindow(); + break; + case "Print": + PrintUtils.startPrintWindow(gBrowser.selectedBrowser.browsingContext); + break; + case "Save": + saveBrowser(gBrowser.selectedBrowser); + break; + case "SendMail": + MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser); + break; + default: + return; + } + evt.stopPropagation(); + evt.preventDefault(); +} + +function gotoHistoryIndex(aEvent) { + aEvent = getRootEvent(aEvent); + + let index = aEvent.target.getAttribute("index"); + if (!index) { + return false; + } + + let where = whereToOpenLink(aEvent); + + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + + try { + gBrowser.gotoIndex(index); + } catch (ex) { + return false; + } + return true; + } + // Modified click. Go there in a new tab/window. + + let historyindex = aEvent.target.getAttribute("historyindex"); + duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex)); + return true; +} + +function BrowserForward(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goForward(); + } catch (ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, 1); + } +} + +function BrowserBack(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goBack(); + } catch (ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, -1); + } +} + +function BrowserHandleBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + BrowserBack(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } +} + +function BrowserHandleShiftBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + BrowserForward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } +} + +function BrowserStop() { + gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); +} + +function BrowserReloadOrDuplicate(aEvent) { + aEvent = getRootEvent(aEvent); + let accelKeyPressed = + AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; + var backgroundTabModifier = aEvent.button == 1 || accelKeyPressed; + + if (aEvent.shiftKey && !backgroundTabModifier) { + BrowserReloadSkipCache(); + return; + } + + let where = whereToOpenLink(aEvent, false, true); + if (where == "current") { + BrowserReload(); + } else { + duplicateTabIn(gBrowser.selectedTab, where); + } +} + +function BrowserReload() { + if (gBrowser.currentURI.schemeIs("view-source")) { + // Bug 1167797: For view source, we always skip the cache + return BrowserReloadSkipCache(); + } + const reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + BrowserReloadWithFlags(reloadFlags); +} + +const kSkipCacheFlags = + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; +function BrowserReloadSkipCache() { + // Bypass proxy and cache. + BrowserReloadWithFlags(kSkipCacheFlags); +} + +function BrowserHome(aEvent) { + if (aEvent && "button" in aEvent && aEvent.button == 2) { + // right-click: do nothing + return; + } + + var homePage = HomePage.get(window); + var where = whereToOpenLink(aEvent, false, true); + var urls; + var notifyObservers; + + // Don't load the home page in pinned or hidden tabs (e.g. Firefox View). + if ( + where == "current" && + (gBrowser?.selectedTab.pinned || gBrowser?.selectedTab.hidden) + ) { + where = "tab"; + } + + // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages + switch (where) { + case "current": + // If we're going to load an initial page in the current tab as the + // home page, we set initialPageLoadedFromURLBar so that the URL + // bar is cleared properly (even during a remoteness flip). + if (isInitialPage(homePage)) { + gBrowser.selectedBrowser.initialPageLoadedFromUserAction = homePage; + } + loadOneOrMoreURIs( + homePage, + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + if (isBlankPageURL(homePage)) { + gURLBar.select(); + } else { + gBrowser.selectedBrowser.focus(); + } + notifyObservers = true; + aEvent?.preventDefault(); + break; + case "tabshifted": + case "tab": + urls = homePage.split("|"); + var loadInBackground = Services.prefs.getBoolPref( + "browser.tabs.loadBookmarksInBackground", + false + ); + // The homepage observer event should only be triggered when the homepage opens + // in the foreground. This is mostly to support the homepage changed by extension + // doorhanger which doesn't currently support background pages. This may change in + // bug 1438396. + notifyObservers = !loadInBackground; + gBrowser.loadTabs(urls, { + inBackground: loadInBackground, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + csp: null, + }); + if (!loadInBackground) { + if (isBlankPageURL(homePage)) { + gURLBar.select(); + } else { + gBrowser.selectedBrowser.focus(); + } + } + aEvent?.preventDefault(); + break; + case "window": + // OpenBrowserWindow will trigger the observer event, so no need to do so here. + notifyObservers = false; + OpenBrowserWindow(); + aEvent?.preventDefault(); + break; + } + if (notifyObservers) { + // A notification for when a user has triggered their homepage. This is used + // to display a doorhanger explaining that an extension has modified the + // homepage, if necessary. Observers are only notified if the homepage + // becomes the active page. + Services.obs.notifyObservers(null, "browser-open-homepage-start"); + } +} + +function loadOneOrMoreURIs(aURIString, aTriggeringPrincipal, aCsp) { + // we're not a browser window, pass the URI string to a new browser window + if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + window.openDialog( + AppConstants.BROWSER_CHROME_URL, + "_blank", + "all,dialog=no", + aURIString + ); + return; + } + + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(aURIString.split("|"), { + inBackground: false, + replace: true, + triggeringPrincipal: aTriggeringPrincipal, + csp: aCsp, + }); + } catch (e) {} +} + +function openLocation(event) { + if (window.location.href == AppConstants.BROWSER_CHROME_URL) { + gURLBar.select(); + gURLBar.view.autoOpen({ event }); + return; + } + + // If there's an open browser window, redirect the command there. + let win = URILoadingHelper.getTargetWindow(window); + if (win) { + win.focus(); + win.openLocation(); + return; + } + + // There are no open browser windows; open a new one. + window.openDialog( + AppConstants.BROWSER_CHROME_URL, + "_blank", + "chrome,all,dialog=no", + BROWSER_NEW_TAB_URL + ); +} + +function BrowserOpenTab({ event, url } = {}) { + let werePassedURL = !!url; + url ??= BROWSER_NEW_TAB_URL; + let searchClipboard = gMiddleClickNewTabUsesPasteboard && event?.button == 1; + + let relatedToCurrent = false; + let where = "tab"; + + if (event) { + where = whereToOpenLink(event, false, true); + + switch (where) { + case "tab": + case "tabshifted": + // When accel-click or middle-click are used, open the new tab as + // related to the current tab. + relatedToCurrent = true; + break; + case "current": + where = "tab"; + break; + } + } + + // A notification intended to be useful for modular peformance tracking + // starting as close as is reasonably possible to the time when the user + // expressed the intent to open a new tab. Since there are a lot of + // entry points, this won't catch every single tab created, but most + // initiated by the user should go through here. + // + // Note 1: This notification gets notified with a promise that resolves + // with the linked browser when the tab gets created + // Note 2: This is also used to notify a user that an extension has changed + // the New Tab page. + Services.obs.notifyObservers( + { + wrappedJSObject: new Promise(resolve => { + let options = { + relatedToCurrent, + resolveOnNewTabCreated: resolve, + }; + if (!werePassedURL && searchClipboard) { + let clipboard = readFromClipboard(); + clipboard = UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard).trim(); + if (clipboard) { + url = clipboard; + options.allowThirdPartyFixup = true; + } + } + openTrustedLinkIn(url, where, options); + }), + }, + "browser-open-newtab-start" + ); +} + +var gLastOpenDirectory = { + _lastDir: null, + get path() { + if (!this._lastDir || !this._lastDir.exists()) { + try { + this._lastDir = Services.prefs.getComplexValue( + "browser.open.lastDir", + Ci.nsIFile + ); + if (!this._lastDir.exists()) { + this._lastDir = null; + } + } catch (e) {} + } + return this._lastDir; + }, + set path(val) { + try { + if (!val || !val.isDirectory()) { + return; + } + } catch (e) { + return; + } + this._lastDir = val.clone(); + + // Don't save the last open directory pref inside the Private Browsing mode + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + Services.prefs.setComplexValue( + "browser.open.lastDir", + Ci.nsIFile, + this._lastDir + ); + } + }, + reset() { + this._lastDir = null; + }, +}; + +function BrowserOpenFileWindow() { + // Get filepicker component. + try { + const nsIFilePicker = Ci.nsIFilePicker; + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + try { + if (fp.file) { + gLastOpenDirectory.path = fp.file.parent.QueryInterface(Ci.nsIFile); + } + } catch (ex) {} + openTrustedLinkIn(fp.fileURL.spec, "current"); + } + }; + + fp.init( + window, + gNavigatorBundle.getString("openFile"), + nsIFilePicker.modeOpen + ); + fp.appendFilters( + nsIFilePicker.filterAll | + nsIFilePicker.filterText | + nsIFilePicker.filterImages | + nsIFilePicker.filterXML | + nsIFilePicker.filterHTML | + nsIFilePicker.filterPDF + ); + fp.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch (ex) {} +} + +function BrowserCloseTabOrWindow(event) { + // If we're not a browser window, just close the window. + if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + closeWindow(true); + return; + } + + // In a multi-select context, close all selected tabs + if (gBrowser.multiSelectedTabsCount) { + gBrowser.removeMultiSelectedTabs(); + return; + } + + // Keyboard shortcuts that would close a tab that is pinned select the first + // unpinned tab instead. + if ( + event && + (event.ctrlKey || event.metaKey || event.altKey) && + gBrowser.selectedTab.pinned + ) { + if (gBrowser.visibleTabs.length > gBrowser._numPinnedTabs) { + gBrowser.tabContainer.selectedIndex = gBrowser._numPinnedTabs; + } + return; + } + + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({ animate: true }); +} + +function BrowserTryToCloseWindow(event) { + if (WindowIsClosing(event)) { + window.close(); + } // WindowIsClosing does all the necessary checks +} + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +function readFromClipboard() { + var url; + + try { + // Create transferable that will transfer the text. + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(getLoadContext()); + + trans.addDataFlavor("text/plain"); + + // If available, use selection clipboard, otherwise global one + let clipboard = Services.clipboard; + if (clipboard.isClipboardTypeSupported(clipboard.kSelectionClipboard)) { + clipboard.getData(trans, clipboard.kSelectionClipboard); + } else { + clipboard.getData(trans, clipboard.kGlobalClipboard); + } + + var data = {}; + trans.getTransferData("text/plain", data); + + if (data) { + data = data.value.QueryInterface(Ci.nsISupportsString); + url = data.data; + } + } catch (ex) {} + + return url; +} + +/** + * Open the View Source dialog. + * + * @param args + * An object with the following properties: + * + * URL (required): + * A string URL for the page we'd like to view the source of. + * browser (optional): + * The browser containing the document that we would like to view the + * source of. This is required if outerWindowID is passed. + * outerWindowID (optional): + * The outerWindowID of the content window containing the document that + * we want to view the source of. You only need to provide this if you + * want to attempt to retrieve the document source from the network + * cache. + * lineNumber (optional): + * The line number to focus on once the source is loaded. + */ +async function BrowserViewSourceOfDocument(args) { + // Check if external view source is enabled. If so, try it. If it fails, + // fallback to internal view source. + if (Services.prefs.getBoolPref("view_source.editor.external")) { + try { + await top.gViewSourceUtils.openInExternalEditor(args); + return; + } catch (data) {} + } + + let tabBrowser = gBrowser; + let preferredRemoteType; + let initialBrowsingContextGroupId; + if (args.browser) { + preferredRemoteType = args.browser.remoteType; + initialBrowsingContextGroupId = args.browser.browsingContext.group.id; + } else { + if (!tabBrowser) { + throw new Error( + "BrowserViewSourceOfDocument should be passed the " + + "subject browser if called from a window without " + + "gBrowser defined." + ); + } + // Some internal URLs (such as specific chrome: and about: URLs that are + // not yet remote ready) cannot be loaded in a remote browser. View + // source in tab expects the new view source browser's remoteness to match + // that of the original URL, so disable remoteness if necessary for this + // URL. + var oa = E10SUtils.predictOriginAttributes({ window }); + preferredRemoteType = E10SUtils.getRemoteTypeForURI( + args.URL, + gMultiProcessBrowser, + gFissionBrowser, + E10SUtils.DEFAULT_REMOTE_TYPE, + null, + oa + ); + } + + // In the case of popups, we need to find a non-popup browser window. + if (!tabBrowser || !window.toolbar.visible) { + // This returns only non-popup browser windows by default. + let browserWindow = BrowserWindowTracker.getTopWindow(); + tabBrowser = browserWindow.gBrowser; + } + + const inNewWindow = !Services.prefs.getBoolPref("view_source.tab"); + + // `viewSourceInBrowser` will load the source content from the page + // descriptor for the tab (when possible) or fallback to the network if + // that fails. Either way, the view source module will manage the tab's + // location, so use "about:blank" here to avoid unnecessary redundant + // requests. + let tab = tabBrowser.addTab("about:blank", { + relatedToCurrent: true, + inBackground: inNewWindow, + skipAnimation: inNewWindow, + preferredRemoteType, + initialBrowsingContextGroupId, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + skipLoad: true, + }); + args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab); + top.gViewSourceUtils.viewSourceInBrowser(args); + + if (inNewWindow) { + tabBrowser.hideTab(tab); + tabBrowser.replaceTabWithWindow(tab); + } +} + +/** + * Opens the View Source dialog for the source loaded in the root + * top-level document of the browser. This is really just a + * convenience wrapper around BrowserViewSourceOfDocument. + * + * @param browser + * The browser that we want to load the source of. + */ +function BrowserViewSource(browser) { + BrowserViewSourceOfDocument({ + browser, + outerWindowID: browser.outerWindowID, + URL: browser.currentURI.spec, + }); +} + +// documentURL - URL of the document to view, or null for this window's document +// initialTab - name of the initial tab to display, or null for the first tab +// imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted +// browsingContext - the browsingContext of the frame that we want to view information about; can be null/omitted +// browser - the browser containing the document we're interested in inspecting; can be null/omitted +function BrowserPageInfo( + documentURL, + initialTab, + imageElement, + browsingContext, + browser +) { + if (HTMLDocument.isInstance(documentURL)) { + Deprecated.warning( + "Please pass the location URL instead of the document " + + "to BrowserPageInfo() as the first argument.", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1238180" + ); + documentURL = documentURL.location; + } + + let args = { initialTab, imageElement, browsingContext, browser }; + + documentURL = documentURL || window.gBrowser.selectedBrowser.currentURI.spec; + + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + + // Check for windows matching the url + for (let currentWindow of Services.wm.getEnumerator("Browser:page-info")) { + if (currentWindow.closed) { + continue; + } + if ( + currentWindow.document.documentElement.getAttribute("relatedUrl") == + documentURL && + PrivateBrowsingUtils.isWindowPrivate(currentWindow) == isPrivate + ) { + currentWindow.focus(); + currentWindow.resetPageInfo(args); + return currentWindow; + } + } + + // We didn't find a matching window, so open a new one. + let options = "chrome,toolbar,dialog=no,resizable"; + + // Ensure the window groups correctly in the Windows taskbar + if (isPrivate) { + options += ",private"; + } + return openDialog( + "chrome://browser/content/pageinfo/pageInfo.xhtml", + "", + options, + args + ); +} + +function UpdateUrlbarSearchSplitterState() { + var splitter = document.getElementById("urlbar-search-splitter"); + var urlbar = document.getElementById("urlbar-container"); + var searchbar = document.getElementById("search-container"); + + if (document.documentElement.getAttribute("customizing") == "true") { + if (splitter) { + splitter.remove(); + } + return; + } + + // If the splitter is already in the right place, we don't need to do anything: + if ( + splitter && + ((splitter.nextElementSibling == searchbar && + splitter.previousElementSibling == urlbar) || + (splitter.nextElementSibling == urlbar && + splitter.previousElementSibling == searchbar)) + ) { + return; + } + + let ibefore = null; + let resizebefore = "none"; + let resizeafter = "none"; + if (urlbar && searchbar) { + if (urlbar.nextElementSibling == searchbar) { + resizeafter = "sibling"; + ibefore = searchbar; + } else if (searchbar.nextElementSibling == urlbar) { + resizebefore = "sibling"; + ibefore = urlbar; + } + } + + if (ibefore) { + if (!splitter) { + splitter = document.createXULElement("splitter"); + splitter.id = "urlbar-search-splitter"; + splitter.setAttribute("resizebefore", resizebefore); + splitter.setAttribute("resizeafter", resizeafter); + splitter.setAttribute("skipintoolbarset", "true"); + splitter.setAttribute("overflows", "false"); + splitter.className = "chromeclass-toolbar-additional"; + } + urlbar.parentNode.insertBefore(splitter, ibefore); + } else if (splitter) { + splitter.remove(); + } +} + +function UpdatePopupNotificationsVisibility() { + // Only need to update PopupNotifications if it has already been initialized + // for this window (i.e. its getter no longer exists). + if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) { + // Notify PopupNotifications that the visible anchors may have changed. This + // also checks the suppression state according to the "shouldSuppress" + // function defined earlier in this file. + PopupNotifications.anchorVisibilityChange(); + } + + // This is similar to the above, but for notifications attached to the + // hamburger menu icon (such as update notifications and add-on install + // notifications.) + PanelUI?.updateNotifications(); +} + +function PageProxyClickHandler(aEvent) { + if (aEvent.button == 1 && Services.prefs.getBoolPref("middlemouse.paste")) { + middleMousePaste(aEvent); + } +} + +/** + * Handle command events bubbling up from error page content + * or from about:newtab or from remote error pages that invoke + * us via async messaging. + */ +var BrowserOnClick = { + async ignoreWarningLink(reason, blockedInfo, browsingContext) { + // Add a notify bar before allowing the user to continue through to the + // site, so that they don't lose track after, e.g., tab switching. + // We can't use browser.contentPrincipal which is principal of about:blocked + // Create one from uri with current principal origin attributes + let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(blockedInfo.uri), + browsingContext.currentWindowGlobal.documentPrincipal.originAttributes + ); + Services.perms.addFromPrincipal( + principal, + "safe-browsing", + Ci.nsIPermissionManager.ALLOW_ACTION, + Ci.nsIPermissionManager.EXPIRE_SESSION + ); + + let buttons = [ + { + label: gNavigatorBundle.getString( + "safebrowsing.getMeOutOfHereButton.label" + ), + accessKey: gNavigatorBundle.getString( + "safebrowsing.getMeOutOfHereButton.accessKey" + ), + callback() { + getMeOutOfHere(browsingContext); + }, + }, + ]; + + let title; + if (reason === "malware") { + let reportUrl = gSafeBrowsing.getReportURL("MalwareMistake", blockedInfo); + title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite"); + // There's no button if we can not get report url, for example if the provider + // of blockedInfo is not Google + if (reportUrl) { + buttons[1] = { + label: gNavigatorBundle.getString( + "safebrowsing.notAnAttackButton.label" + ), + accessKey: gNavigatorBundle.getString( + "safebrowsing.notAnAttackButton.accessKey" + ), + callback() { + openTrustedLinkIn(reportUrl, "tab"); + }, + }; + } + } else if (reason === "phishing") { + let reportUrl = gSafeBrowsing.getReportURL("PhishMistake", blockedInfo); + title = gNavigatorBundle.getString("safebrowsing.deceptiveSite"); + // There's no button if we can not get report url, for example if the provider + // of blockedInfo is not Google + if (reportUrl) { + buttons[1] = { + label: gNavigatorBundle.getString( + "safebrowsing.notADeceptiveSiteButton.label" + ), + accessKey: gNavigatorBundle.getString( + "safebrowsing.notADeceptiveSiteButton.accessKey" + ), + callback() { + openTrustedLinkIn(reportUrl, "tab"); + }, + }; + } + } else if (reason === "unwanted") { + title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite"); + // There is no button for reporting errors since Google doesn't currently + // provide a URL endpoint for these reports. + } else if (reason === "harmful") { + title = gNavigatorBundle.getString("safebrowsing.reportedHarmfulSite"); + // There is no button for reporting errors since Google doesn't currently + // provide a URL endpoint for these reports. + } + + await SafeBrowsingNotificationBox.show(title, buttons); + + // Allow users to override and continue through to the site. + // Note that we have to use the passed URI info and can't just + // rely on the document URI, because the latter contains + // additional query parameters that should be stripped. + let triggeringPrincipal = + blockedInfo.triggeringPrincipal || + _createNullPrincipalFromTabUserContextId(); + + browsingContext.fixupAndLoadURIString(blockedInfo.uri, { + triggeringPrincipal, + flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, + }); + }, +}; + +/** + * Re-direct the browser to a known-safe page. This function is + * used when, for example, the user browses to a known malware page + * and is presented with about:blocked. The "Get me out of here!" + * button should take the user to the default start page so that even + * when their own homepage is infected, we can get them somewhere safe. + */ +function getMeOutOfHere(browsingContext) { + browsingContext.top.fixupAndLoadURIString(getDefaultHomePage(), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), // Also needs to load homepage + }); +} + +/** + * Return the default start page for the cases when the user's own homepage is + * infected, so we can get them somewhere safe. + */ +function getDefaultHomePage() { + let url = BROWSER_NEW_TAB_URL; + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + return url; + } + url = HomePage.getDefault(); + // If url is a pipe-delimited set of pages, just take the first one. + if (url.includes("|")) { + url = url.split("|")[0]; + } + return url; +} + +function BrowserFullScreen() { + window.fullScreen = !window.fullScreen || BrowserHandler.kiosk; +} + +function BrowserReloadWithFlags(reloadFlags) { + let unchangedRemoteness = []; + + for (let tab of gBrowser.selectedTabs) { + let browser = tab.linkedBrowser; + let url = browser.currentURI; + let urlSpec = url.spec; + // We need to cache the content principal here because the browser will be + // reconstructed when the remoteness changes and the content prinicpal will + // be cleared after reconstruction. + let principal = tab.linkedBrowser.contentPrincipal; + if (gBrowser.updateBrowserRemotenessByURL(browser, urlSpec)) { + // If the remoteness has changed, the new browser doesn't have any + // information of what was loaded before, so we need to load the previous + // URL again. + if (tab.linkedPanel) { + loadBrowserURI(browser, url, principal); + } else { + // Shift to fully loaded browser and make + // sure load handler is instantiated. + tab.addEventListener( + "SSTabRestoring", + () => loadBrowserURI(browser, url, principal), + { once: true } + ); + gBrowser._insertBrowser(tab); + } + } else { + unchangedRemoteness.push(tab); + } + } + + if (!unchangedRemoteness.length) { + return; + } + + // Reset temporary permissions on the remaining tabs to reload. + // This is done here because we only want to reset + // permissions on user reload. + for (let tab of unchangedRemoteness) { + SitePermissions.clearTemporaryBlockPermissions(tab.linkedBrowser); + // Also reset DOS mitigations for the basic auth prompt on reload. + delete tab.linkedBrowser.authPromptAbuseCounter; + } + gIdentityHandler.hidePopup(); + gPermissionPanel.hidePopup(); + + let handlingUserInput = document.hasValidTransientUserGestureActivation; + + for (let tab of unchangedRemoteness) { + if (tab.linkedPanel) { + sendReloadMessage(tab); + } else { + // Shift to fully loaded browser and make + // sure load handler is instantiated. + tab.addEventListener("SSTabRestoring", () => sendReloadMessage(tab), { + once: true, + }); + gBrowser._insertBrowser(tab); + } + } + + function loadBrowserURI(browser, url, principal) { + browser.loadURI(url, { + flags: reloadFlags, + triggeringPrincipal: principal, + }); + } + + function sendReloadMessage(tab) { + tab.linkedBrowser.sendMessageToActor( + "Browser:Reload", + { flags: reloadFlags, handlingUserInput }, + "BrowserTab" + ); + } +} + +// TODO: can we pull getPEMString in from pippki.js instead of +// duplicating them here? +function getPEMString(cert) { + var derb64 = cert.getBase64DERString(); + // Wrap the Base64 string into lines of 64 characters, + // with CRLF line breaks (as specified in RFC 1421). + var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n"); + return ( + "-----BEGIN CERTIFICATE-----\r\n" + + wrapped + + "\r\n-----END CERTIFICATE-----\r\n" + ); +} + +var browserDragAndDrop = { + canDropLink: aEvent => Services.droppedLinkHandler.canDropLink(aEvent, true), + + dragOver(aEvent) { + if (this.canDropLink(aEvent)) { + aEvent.preventDefault(); + } + }, + + getTriggeringPrincipal(aEvent) { + return Services.droppedLinkHandler.getTriggeringPrincipal(aEvent); + }, + + getCsp(aEvent) { + return Services.droppedLinkHandler.getCsp(aEvent); + }, + + validateURIsForDrop(aEvent, aURIs) { + return Services.droppedLinkHandler.validateURIsForDrop(aEvent, aURIs); + }, + + dropLinks(aEvent, aDisallowInherit) { + return Services.droppedLinkHandler.dropLinks(aEvent, aDisallowInherit); + }, +}; + +var homeButtonObserver = { + onDrop(aEvent) { + // disallow setting home pages that inherit the principal + let links = browserDragAndDrop.dropLinks(aEvent, true); + if (links.length) { + let urls = []; + for (let link of links) { + if (link.url.includes("|")) { + urls.push(...link.url.split("|")); + } else { + urls.push(link.url); + } + } + + try { + browserDragAndDrop.validateURIsForDrop(aEvent, urls); + } catch (e) { + return; + } + + setTimeout(openHomeDialog, 0, urls.join("|")); + } + }, + + onDragOver(aEvent) { + if (HomePage.locked) { + return; + } + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, +}; + +function openHomeDialog(aURL) { + var promptTitle = gNavigatorBundle.getString("droponhometitle"); + var promptMsg; + if (aURL.includes("|")) { + promptMsg = gNavigatorBundle.getString("droponhomemsgMultiple"); + } else { + promptMsg = gNavigatorBundle.getString("droponhomemsg"); + } + + var pressedVal = Services.prompt.confirmEx( + window, + promptTitle, + promptMsg, + Services.prompt.STD_YES_NO_BUTTONS, + null, + null, + null, + null, + { value: 0 } + ); + + if (pressedVal == 0) { + HomePage.set(aURL).catch(console.error); + } +} + +var newTabButtonObserver = { + onDragOver(aEvent) { + browserDragAndDrop.dragOver(aEvent); + }, + async onDrop(aEvent) { + let links = browserDragAndDrop.dropLinks(aEvent); + if ( + links.length >= + Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn") + ) { + // Sync dialog cannot be used inside drop event handler. + let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs( + links.length, + window + ); + if (!answer) { + return; + } + } + + let where = aEvent.shiftKey ? "tabshifted" : "tab"; + let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent); + let csp = browserDragAndDrop.getCsp(aEvent); + for (let link of links) { + if (link.url) { + let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url); + // Allow third-party services to fixup this URL. + openLinkIn(data.url, where, { + postData: data.postData, + allowThirdPartyFixup: true, + triggeringPrincipal, + csp, + }); + } + } + }, +}; + +var newWindowButtonObserver = { + onDragOver(aEvent) { + browserDragAndDrop.dragOver(aEvent); + }, + async onDrop(aEvent) { + let links = browserDragAndDrop.dropLinks(aEvent); + if ( + links.length >= + Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn") + ) { + // Sync dialog cannot be used inside drop event handler. + let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs( + links.length, + window + ); + if (!answer) { + return; + } + } + + let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent); + let csp = browserDragAndDrop.getCsp(aEvent); + for (let link of links) { + if (link.url) { + let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url); + // Allow third-party services to fixup this URL. + openLinkIn(data.url, "window", { + // TODO fix allowInheritPrincipal + // (this is required by javascript: drop to the new window) Bug 1475201 + allowInheritPrincipal: true, + postData: data.postData, + allowThirdPartyFixup: true, + triggeringPrincipal, + csp, + }); + } + } + }, +}; + +const BrowserSearch = { + _searchInitComplete: false, + + init() { + Services.obs.addObserver(this, "browser-search-engine-modified"); + }, + + delayedStartupInit() { + // Asynchronously initialize the search service if necessary, to get the + // current engine for working out the placeholder. + this._updateURLBarPlaceholderFromDefaultEngine( + PrivateBrowsingUtils.isWindowPrivate(window), + // Delay the update for this until so that we don't change it while + // the user is looking at it / isn't expecting it. + true + ).then(() => { + this._searchInitComplete = true; + }); + }, + + uninit() { + Services.obs.removeObserver(this, "browser-search-engine-modified"); + }, + + observe(engine, topic, data) { + // There are two kinds of search engine objects, nsISearchEngine objects and + // plain { uri, title, icon } objects. `engine` in this method is the + // former. The browser.engines and browser.hiddenEngines arrays are the + // latter, and they're the engines offered by the the page in the browser. + // + // The two types of engines are currently related by their titles/names, + // although that may change; see bug 335102. + let engineName = engine.wrappedJSObject.name; + switch (data) { + case "engine-removed": + // An engine was removed from the search service. If a page is offering + // the engine, then the engine needs to be added back to the corresponding + // browser's offered engines. + this._addMaybeOfferedEngine(engineName); + break; + case "engine-added": + // An engine was added to the search service. If a page is offering the + // engine, then the engine needs to be removed from the corresponding + // browser's offered engines. + this._removeMaybeOfferedEngine(engineName); + break; + case "engine-default": + if ( + this._searchInitComplete && + !PrivateBrowsingUtils.isWindowPrivate(window) + ) { + this._updateURLBarPlaceholder(engineName, false); + } + break; + case "engine-default-private": + if ( + this._searchInitComplete && + PrivateBrowsingUtils.isWindowPrivate(window) + ) { + this._updateURLBarPlaceholder(engineName, true); + } + break; + } + }, + + _addMaybeOfferedEngine(engineName) { + let selectedBrowserOffersEngine = false; + for (let browser of gBrowser.browsers) { + for (let i = 0; i < (browser.hiddenEngines || []).length; i++) { + if (browser.hiddenEngines[i].title == engineName) { + if (!browser.engines) { + browser.engines = []; + } + browser.engines.push(browser.hiddenEngines[i]); + browser.hiddenEngines.splice(i, 1); + if (browser == gBrowser.selectedBrowser) { + selectedBrowserOffersEngine = true; + } + break; + } + } + } + if (selectedBrowserOffersEngine) { + this.updateOpenSearchBadge(); + } + }, + + _removeMaybeOfferedEngine(engineName) { + let selectedBrowserOffersEngine = false; + for (let browser of gBrowser.browsers) { + for (let i = 0; i < (browser.engines || []).length; i++) { + if (browser.engines[i].title == engineName) { + if (!browser.hiddenEngines) { + browser.hiddenEngines = []; + } + browser.hiddenEngines.push(browser.engines[i]); + browser.engines.splice(i, 1); + if (browser == gBrowser.selectedBrowser) { + selectedBrowserOffersEngine = true; + } + break; + } + } + } + if (selectedBrowserOffersEngine) { + this.updateOpenSearchBadge(); + } + }, + + /** + * Initializes the urlbar placeholder to the pre-saved engine name. We do this + * via a preference, to avoid needing to synchronously init the search service. + * + * This should be called around the time of DOMContentLoaded, so that it is + * initialized quickly before the user sees anything. + * + * Note: If the preference doesn't exist, we don't do anything as the default + * placeholder is a string which doesn't have the engine name; however, this + * can be overridden using the `force` parameter. + * + * @param {Boolean} force If true and the preference doesn't exist, the + * placeholder will be set to the default version + * without an engine name ("Search or enter address"). + */ + initPlaceHolder(force = false) { + const prefName = + "browser.urlbar.placeholderName" + + (PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : ""); + let engineName = Services.prefs.getStringPref(prefName, ""); + if (engineName || force) { + // We can do this directly, since we know we're at DOMContentLoaded. + this._setURLBarPlaceholder(engineName); + } + }, + + /** + * This is a wrapper around '_updateURLBarPlaceholder' that uses the + * appropriate default engine to get the engine name. + * + * @param {Boolean} isPrivate Set to true if this is a private window. + * @param {Boolean} [delayUpdate] Set to true, to delay update until the + * placeholder is not displayed. + */ + async _updateURLBarPlaceholderFromDefaultEngine( + isPrivate, + delayUpdate = false + ) { + const getDefault = isPrivate + ? Services.search.getDefaultPrivate + : Services.search.getDefault; + let defaultEngine = await getDefault(); + if (!this._searchInitComplete) { + // If we haven't finished initialising, ensure the placeholder + // preference is set for the next startup. + SearchUIUtils.updatePlaceholderNamePreference(defaultEngine, isPrivate); + } + this._updateURLBarPlaceholder(defaultEngine.name, isPrivate, delayUpdate); + }, + + /** + * Updates the URLBar placeholder for the specified engine, delaying the + * update if required. This also saves the current engine name in preferences + * for the next restart. + * + * Note: The engine name will only be displayed for built-in engines, as we + * know they should have short names. + * + * @param {String} engineName The search engine name to use for the update. + * @param {Boolean} isPrivate Set to true if this is a private window. + * @param {Boolean} [delayUpdate] Set to true, to delay update until the + * placeholder is not displayed. + */ + _updateURLBarPlaceholder(engineName, isPrivate, delayUpdate = false) { + if (!engineName) { + throw new Error("Expected an engineName to be specified"); + } + + const engine = Services.search.getEngineByName(engineName); + if (!engine.isAppProvided) { + // Set the engine name to an empty string for non-default engines, which'll + // make sure we display the default placeholder string. + engineName = ""; + } + + // Only delay if requested, and we're not displaying text in the URL bar + // currently. + if (delayUpdate && !gURLBar.value) { + // Delays changing the URL Bar placeholder until the user is not going to be + // seeing it, e.g. when there is a value entered in the bar, or if there is + // a tab switch to a tab which has a url loaded. We delay the update until + // the user is out of search mode since an alternative placeholder is used + // in search mode. + let placeholderUpdateListener = () => { + if (gURLBar.value && !gURLBar.searchMode) { + // By the time the user has switched, they may have changed the engine + // again, so we need to call this function again but with the + // new engine name. + // No need to await for this to finish, we're in a listener here anyway. + this._updateURLBarPlaceholderFromDefaultEngine(isPrivate, false); + gURLBar.removeEventListener("input", placeholderUpdateListener); + gBrowser.tabContainer.removeEventListener( + "TabSelect", + placeholderUpdateListener + ); + } + }; + + gURLBar.addEventListener("input", placeholderUpdateListener); + gBrowser.tabContainer.addEventListener( + "TabSelect", + placeholderUpdateListener + ); + } else if (!gURLBar.searchMode) { + this._setURLBarPlaceholder(engineName); + } + }, + + /** + * Sets the URLBar placeholder to either something based on the engine name, + * or the default placeholder. + * + * @param {String} name The name of the engine to use, an empty string if to + * use the default placeholder. + */ + _setURLBarPlaceholder(name) { + document.l10n.setAttributes( + gURLBar.inputField, + name ? "urlbar-placeholder-with-name" : "urlbar-placeholder", + name ? { name } : undefined + ); + }, + + addEngine(browser, engine) { + if (!this._searchInitComplete) { + // We haven't finished initialising search yet. This means we can't + // call getEngineByName here. Since this is only on start-up and unlikely + // to happen in the normal case, we'll just return early rather than + // trying to handle it asynchronously. + return; + } + // Check to see whether we've already added an engine with this title + if (browser.engines) { + if (browser.engines.some(e => e.title == engine.title)) { + return; + } + } + + var hidden = false; + // If this engine (identified by title) is already in the list, add it + // to the list of hidden engines rather than to the main list. + if (Services.search.getEngineByName(engine.title)) { + hidden = true; + } + + var engines = (hidden ? browser.hiddenEngines : browser.engines) || []; + + engines.push({ + uri: engine.href, + title: engine.title, + get icon() { + return browser.mIconURL; + }, + }); + + if (hidden) { + browser.hiddenEngines = engines; + } else { + browser.engines = engines; + if (browser == gBrowser.selectedBrowser) { + this.updateOpenSearchBadge(); + } + } + }, + + /** + * Update the browser UI to show whether or not additional engines are + * available when a page is loaded or the user switches tabs to a page that + * has search engines. + */ + updateOpenSearchBadge() { + gURLBar.addSearchEngineHelper.setEnginesFromBrowser( + gBrowser.selectedBrowser + ); + + var searchBar = this.searchBar; + if (!searchBar) { + return; + } + + var engines = gBrowser.selectedBrowser.engines; + if (engines && engines.length) { + searchBar.setAttribute("addengines", "true"); + } else { + searchBar.removeAttribute("addengines"); + } + }, + + /** + * Focuses the search bar if present on the toolbar, or the address bar, + * putting it in search mode. Will do so in an existing non-popup browser + * window or open a new one if necessary. + */ + webSearch: function BrowserSearch_webSearch() { + if ( + window.location.href != AppConstants.BROWSER_CHROME_URL || + gURLBar.readOnly + ) { + let win = URILoadingHelper.getTopWin(window, { skipPopups: true }); + if (win) { + // If there's an open browser window, it should handle this command + win.focus(); + win.BrowserSearch.webSearch(); + } else { + // If there are no open browser windows, open a new one + var observer = function (subject, topic, data) { + if (subject == win) { + BrowserSearch.webSearch(); + Services.obs.removeObserver( + observer, + "browser-delayed-startup-finished" + ); + } + }; + win = window.openDialog( + AppConstants.BROWSER_CHROME_URL, + "_blank", + "chrome,all,dialog=no", + "about:blank" + ); + Services.obs.addObserver(observer, "browser-delayed-startup-finished"); + } + return; + } + + let focusUrlBarIfSearchFieldIsNotActive = function (aSearchBar) { + if (!aSearchBar || document.activeElement != aSearchBar.textbox) { + // Limit the results to search suggestions, like the search bar. + gURLBar.searchModeShortcut(); + } + }; + + let searchBar = this.searchBar; + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + let focusSearchBar = () => { + searchBar = this.searchBar; + searchBar.select(); + focusUrlBarIfSearchFieldIsNotActive(searchBar); + }; + if ( + placement && + searchBar && + ((searchBar.parentNode.getAttribute("overflowedItem") == "true" && + placement.area == CustomizableUI.AREA_NAVBAR) || + placement.area == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) + ) { + let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR); + navBar.overflowable.show().then(focusSearchBar); + return; + } + if (searchBar) { + if (window.fullScreen) { + FullScreen.showNavToolbox(); + } + searchBar.select(); + } + focusUrlBarIfSearchFieldIsNotActive(searchBar); + }, + + /** + * Loads a search results page, given a set of search terms. Uses the current + * engine if the search bar is visible, or the default engine otherwise. + * + * @param searchText + * The search terms to use for the search. + * @param where + * String indicating where the search should load. Most commonly used + * are 'tab' or 'window', defaults to 'current'. + * @param usePrivate + * Whether to use the Private Browsing mode default search engine. + * Defaults to `false`. + * @param purpose [optional] + * A string meant to indicate the context of the search request. This + * allows the search service to provide a different nsISearchSubmission + * depending on e.g. where the search is triggered in the UI. + * @param triggeringPrincipal + * The principal to use for a new window or tab. + * @param csp + * The content security policy to use for a new window or tab. + * @param inBackground [optional] + * Set to true for the tab to be loaded in the background, default false. + * @param engine [optional] + * The search engine to use for the search. + * @param tab [optional] + * The tab to show the search result. + * + * @return engine The search engine used to perform a search, or null if no + * search was performed. + */ + async _loadSearch( + searchText, + where, + usePrivate, + purpose, + triggeringPrincipal, + csp, + inBackground = false, + engine = null, + tab = null + ) { + if (!triggeringPrincipal) { + throw new Error( + "Required argument triggeringPrincipal missing within _loadSearch" + ); + } + + if (!engine) { + engine = usePrivate + ? await Services.search.getDefaultPrivate() + : await Services.search.getDefault(); + } + + let submission = engine.getSubmission(searchText, null, purpose); // HTML response + + // getSubmission can return null if the engine doesn't have a URL + // with a text/html response type. This is unlikely (since + // SearchService._addEngineToStore() should fail for such an engine), + // but let's be on the safe side. + if (!submission) { + return null; + } + + openLinkIn(submission.uri.spec, where || "current", { + private: usePrivate && !PrivateBrowsingUtils.isWindowPrivate(window), + postData: submission.postData, + inBackground, + relatedToCurrent: true, + triggeringPrincipal, + csp, + targetBrowser: tab?.linkedBrowser, + globalHistoryOptions: { + triggeringSearchEngine: engine.name, + }, + }); + + return { engine, url: submission.uri }; + }, + + /** + * Perform a search initiated from the context menu. + * + * This should only be called from the context menu. See + * BrowserSearch.loadSearch for the preferred API. + */ + async loadSearchFromContext( + terms, + usePrivate, + triggeringPrincipal, + csp, + event + ) { + event = getRootEvent(event); + let where = whereToOpenLink(event); + if (where == "current") { + // override: historically search opens in new tab + where = "tab"; + } + if (usePrivate && !PrivateBrowsingUtils.isWindowPrivate(window)) { + where = "window"; + } + let inBackground = Services.prefs.getBoolPref( + "browser.search.context.loadInBackground" + ); + if (event.button == 1 || event.ctrlKey) { + inBackground = !inBackground; + } + + let { engine } = await BrowserSearch._loadSearch( + terms, + where, + usePrivate, + "contextmenu", + Services.scriptSecurityManager.createNullPrincipal( + triggeringPrincipal.originAttributes + ), + csp, + inBackground + ); + + if (engine) { + BrowserSearchTelemetry.recordSearch( + gBrowser.selectedBrowser, + engine, + "contextmenu" + ); + } + }, + + /** + * Perform a search initiated from the command line. + */ + async loadSearchFromCommandLine(terms, usePrivate, triggeringPrincipal, csp) { + let { engine } = await BrowserSearch._loadSearch( + terms, + "current", + usePrivate, + "system", + triggeringPrincipal, + csp + ); + if (engine) { + BrowserSearchTelemetry.recordSearch( + gBrowser.selectedBrowser, + engine, + "system" + ); + } + }, + + /** + * Perform a search initiated from an extension. + */ + async loadSearchFromExtension({ + query, + engine, + where, + tab, + triggeringPrincipal, + }) { + const result = await BrowserSearch._loadSearch( + query, + where, + PrivateBrowsingUtils.isWindowPrivate(window), + "webextension", + triggeringPrincipal, + null, + false, + engine, + tab + ); + + BrowserSearchTelemetry.recordSearch( + gBrowser.selectedBrowser, + result.engine, + "webextension" + ); + }, + + /** + * Returns the search bar element if it is present in the toolbar, null otherwise. + */ + get searchBar() { + return document.getElementById("searchbar"); + }, + + /** + * Infobar to notify the user's search engine has been removed + * and replaced with an application default search engine. + * + * @param {string} oldEngine + * name of the engine to be moved and replaced. + * @param {string} newEngine + * name of the application default engine to replaced the removed engine. + */ + async removalOfSearchEngineNotificationBox(oldEngine, newEngine) { + let buttons = [ + { + "l10n-id": "remove-search-engine-button", + primary: true, + callback() { + const notificationBox = gNotificationBox.getNotificationWithValue( + "search-engine-removal" + ); + gNotificationBox.removeNotification(notificationBox); + }, + }, + { + supportPage: "search-engine-removal", + }, + ]; + + await gNotificationBox.appendNotification( + "search-engine-removal", + { + label: { + "l10n-id": "removed-search-engine-message2", + "l10n-args": { oldEngine, newEngine }, + }, + priority: gNotificationBox.PRIORITY_SYSTEM, + }, + buttons + ); + + // Update engine name in the placeholder to the new default engine name. + this._updateURLBarPlaceholderFromDefaultEngine( + PrivateBrowsingUtils.isWindowPrivate(window), + false + ).catch(console.error); + }, +}; + +XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch); + +function CreateContainerTabMenu(event) { + // Do not open context menus within menus. + // Note that triggerNode is null if we're opened by long press. + if (event.target.triggerNode?.closest("menupopup")) { + return false; + } + createUserContextMenu(event, { + useAccessKeys: false, + showDefaultTab: true, + }); +} + +function FillHistoryMenu(aParent) { + // Lazily add the hover listeners on first showing and never remove them + if (!aParent.hasStatusListener) { + // Show history item's uri in the status bar when hovering, and clear on exit + aParent.addEventListener("DOMMenuItemActive", function (aEvent) { + // Only the current page should have the checked attribute, so skip it + if (!aEvent.target.hasAttribute("checked")) { + XULBrowserWindow.setOverLink(aEvent.target.getAttribute("uri")); + } + }); + aParent.addEventListener("DOMMenuItemInactive", function () { + XULBrowserWindow.setOverLink(""); + }); + + aParent.hasStatusListener = true; + } + + // Remove old entries if any + let children = aParent.children; + for (var i = children.length - 1; i >= 0; --i) { + if (children[i].hasAttribute("index")) { + aParent.removeChild(children[i]); + } + } + + const MAX_HISTORY_MENU_ITEMS = 15; + + const tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); + const tooltipCurrent = gNavigatorBundle.getString("tabHistory.reloadCurrent"); + const tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); + + function updateSessionHistory(sessionHistory, initial, ssInParent) { + let count = ssInParent + ? sessionHistory.count + : sessionHistory.entries.length; + + if (!initial) { + if (count <= 1) { + // if there is only one entry now, close the popup. + aParent.hidePopup(); + return; + } else if (aParent.id != "backForwardMenu" && !aParent.parentNode.open) { + // if the popup wasn't open before, but now needs to be, reopen the menu. + // It should trigger FillHistoryMenu again. This might happen with the + // delay from click-and-hold menus but skip this for the context menu + // (backForwardMenu) rather than figuring out how the menu should be + // positioned and opened as it is an extreme edgecase. + aParent.parentNode.open = true; + return; + } + } + + let index = sessionHistory.index; + let half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); + let start = Math.max(index - half_length, 0); + let end = Math.min( + start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1, + count + ); + if (end == count) { + start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0); + } + + let existingIndex = 0; + + for (let j = end - 1; j >= start; j--) { + let entry = ssInParent + ? sessionHistory.getEntryAtIndex(j) + : sessionHistory.entries[j]; + // Explicitly check for "false" to stay backwards-compatible with session histories + // from before the hasUserInteraction was implemented. + if ( + BrowserUtils.navigationRequireUserInteraction && + entry.hasUserInteraction === false && + // Always allow going to the first and last navigation points. + j != end - 1 && + j != start + ) { + continue; + } + let uri = ssInParent ? entry.URI.spec : entry.url; + + let item = + existingIndex < children.length + ? children[existingIndex] + : document.createXULElement("menuitem"); + + item.setAttribute("uri", uri); + item.setAttribute("label", entry.title || uri); + item.setAttribute("index", j); + + // Cache this so that gotoHistoryIndex doesn't need the original index + item.setAttribute("historyindex", j - index); + + if (j != index) { + // Use list-style-image rather than the image attribute in order to + // allow CSS to override this. + item.style.listStyleImage = `url(page-icon:${uri})`; + } + + if (j < index) { + item.className = + "unified-nav-back menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipBack); + } else if (j == index) { + item.setAttribute("type", "radio"); + item.setAttribute("checked", "true"); + item.className = "unified-nav-current"; + item.setAttribute("tooltiptext", tooltipCurrent); + } else { + item.className = + "unified-nav-forward menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipForward); + } + + if (!item.parentNode) { + aParent.appendChild(item); + } + + existingIndex++; + } + + if (!initial) { + let existingLength = children.length; + while (existingIndex < existingLength) { + aParent.removeChild(aParent.lastElementChild); + existingIndex++; + } + } + } + + // If session history in parent is available, use it. Otherwise, get the session history + // from session store. + let sessionHistory = gBrowser.selectedBrowser.browsingContext.sessionHistory; + if (sessionHistory?.count) { + // Don't show the context menu if there is only one item. + if (sessionHistory.count <= 1) { + return false; + } + + updateSessionHistory(sessionHistory, true, true); + } else { + sessionHistory = SessionStore.getSessionHistory( + gBrowser.selectedTab, + updateSessionHistory + ); + updateSessionHistory(sessionHistory, true, false); + } + + return true; +} + +function BrowserDownloadsUI() { + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + openTrustedLinkIn("about:downloads", "tab"); + } else { + PlacesCommandHook.showPlacesOrganizer("Downloads"); + } +} + +function toOpenWindowByType(inType, uri, features) { + var topWindow = Services.wm.getMostRecentWindow(inType); + + if (topWindow) { + topWindow.focus(); + } else if (features) { + window.open(uri, "_blank", features); + } else { + window.open( + uri, + "_blank", + "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar" + ); + } +} + +/** + * Open a new browser window. See `BrowserWindowTracker.openWindow` for + * options. + * + * @return a reference to the new window. + */ +function OpenBrowserWindow(options = {}) { + return BrowserWindowTracker.openWindow({ openerWindow: window, ...options }); +} + +/** + * Update the global flag that tracks whether or not any edit UI (the Edit menu, + * edit-related items in the context menu, and edit-related toolbar buttons + * is visible, then update the edit commands' enabled state accordingly. We use + * this flag to skip updating the edit commands on focus or selection changes + * when no UI is visible to improve performance (including pageload performance, + * since focus changes when you load a new page). + * + * If UI is visible, we use goUpdateGlobalEditMenuItems to set the commands' + * enabled state so the UI will reflect it appropriately. + * + * If the UI isn't visible, we enable all edit commands so keyboard shortcuts + * still work and just lazily disable them as needed when the user presses a + * shortcut. + * + * This doesn't work on Mac, since Mac menus flash when users press their + * keyboard shortcuts, so edit UI is essentially always visible on the Mac, + * and we need to always update the edit commands. Thus on Mac this function + * is a no op. + */ +function updateEditUIVisibility() { + if (AppConstants.platform == "macosx") { + return; + } + + let editMenuPopupState = document.getElementById("menu_EditPopup").state; + let contextMenuPopupState = document.getElementById( + "contentAreaContextMenu" + ).state; + let placesContextMenuPopupState = + document.getElementById("placesContext").state; + + let oldVisible = gEditUIVisible; + + // The UI is visible if the Edit menu is opening or open, if the context menu + // is open, or if the toolbar has been customized to include the Cut, Copy, + // or Paste toolbar buttons. + gEditUIVisible = + editMenuPopupState == "showing" || + editMenuPopupState == "open" || + contextMenuPopupState == "showing" || + contextMenuPopupState == "open" || + placesContextMenuPopupState == "showing" || + placesContextMenuPopupState == "open"; + const kOpenPopupStates = ["showing", "open"]; + if (!gEditUIVisible) { + // Now check the edit-controls toolbar buttons. + let placement = CustomizableUI.getPlacementOfWidget("edit-controls"); + let areaType = placement ? CustomizableUI.getAreaType(placement.area) : ""; + if (areaType == CustomizableUI.TYPE_PANEL) { + let customizablePanel = PanelUI.overflowPanel; + gEditUIVisible = kOpenPopupStates.includes(customizablePanel.state); + } else if ( + areaType == CustomizableUI.TYPE_TOOLBAR && + window.toolbar.visible + ) { + // The edit controls are on a toolbar, so they are visible, + // unless they're in a panel that isn't visible... + if (placement.area == "nav-bar") { + let editControls = document.getElementById("edit-controls"); + gEditUIVisible = + !editControls.hasAttribute("overflowedItem") || + kOpenPopupStates.includes( + document.getElementById("widget-overflow").state + ); + } else { + gEditUIVisible = true; + } + } + } + + // Now check the main menu panel + if (!gEditUIVisible) { + gEditUIVisible = kOpenPopupStates.includes(PanelUI.panel.state); + } + + // No need to update commands if the edit UI visibility has not changed. + if (gEditUIVisible == oldVisible) { + return; + } + + // If UI is visible, update the edit commands' enabled state to reflect + // whether or not they are actually enabled for the current focus/selection. + if (gEditUIVisible) { + goUpdateGlobalEditMenuItems(); + } else { + // Otherwise, enable all commands, so that keyboard shortcuts still work, + // then lazily determine their actual enabled state when the user presses + // a keyboard shortcut. + goSetCommandEnabled("cmd_undo", true); + goSetCommandEnabled("cmd_redo", true); + goSetCommandEnabled("cmd_cut", true); + goSetCommandEnabled("cmd_copy", true); + goSetCommandEnabled("cmd_paste", true); + goSetCommandEnabled("cmd_selectAll", true); + goSetCommandEnabled("cmd_delete", true); + goSetCommandEnabled("cmd_switchTextDirection", true); + } +} + +let gFileMenu = { + /** + * Updates User Context Menu Item UI visibility depending on + * privacy.userContext.enabled pref state. + */ + updateUserContextUIVisibility() { + let menu = document.getElementById("menu_newUserContext"); + menu.hidden = !Services.prefs.getBoolPref( + "privacy.userContext.enabled", + false + ); + // Visibility of File menu item shouldn't change frequently. + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + menu.setAttribute("disabled", "true"); + } + }, + + /** + * Updates the enabled state of the "Import From Another Browser" command + * depending on the DisableProfileImport policy. + */ + updateImportCommandEnabledState() { + if (!Services.policies.isAllowed("profileImport")) { + document + .getElementById("cmd_file_importFromAnotherBrowser") + .setAttribute("disabled", "true"); + } + }, + + /** + * Updates the "Close tab" command to reflect the number of selected tabs, + * when applicable. + */ + updateTabCloseCountState() { + document.l10n.setAttributes( + document.getElementById("menu_close"), + "menu-file-close-tab", + { tabCount: gBrowser.selectedTabs.length } + ); + }, + + onPopupShowing(event) { + // We don't care about submenus: + if (event.target.id != "menu_FilePopup") { + return; + } + this.updateUserContextUIVisibility(); + this.updateImportCommandEnabledState(); + this.updateTabCloseCountState(); + if (AppConstants.platform == "macosx") { + gShareUtils.updateShareURLMenuItem( + gBrowser.selectedBrowser, + document.getElementById("menu_savePage") + ); + } + PrintUtils.updatePrintSetupMenuHiddenState(); + }, +}; + +let gShareUtils = { + /** + * Updates a sharing item in a given menu, creating it if necessary. + */ + updateShareURLMenuItem(browser, insertAfterEl) { + if (!Services.prefs.getBoolPref("browser.menu.share_url.allow", true)) { + return; + } + + // We only support "share URL" on macOS and on Windows: + if (AppConstants.platform != "macosx" && AppConstants.platform != "win") { + return; + } + + let shareURL = insertAfterEl.nextElementSibling; + if (!shareURL?.matches(".share-tab-url-item")) { + shareURL = this._createShareURLMenuItem(insertAfterEl); + } + + shareURL.browserToShare = Cu.getWeakReference(browser); + if (AppConstants.platform == "win") { + // We disable the item on Windows, as there's no submenu. + // On macOS, we handle this inside the menupopup. + shareURL.hidden = !BrowserUtils.getShareableURL(browser.currentURI); + } + }, + + /** + * Creates and returns the "Share" menu item. + */ + _createShareURLMenuItem(insertAfterEl) { + let menu = insertAfterEl.parentNode; + let shareURL = null; + if (AppConstants.platform == "win") { + shareURL = this._buildShareURLItem(menu.id); + } else if (AppConstants.platform == "macosx") { + shareURL = this._buildShareURLMenu(menu.id); + } + shareURL.className = "share-tab-url-item"; + + let l10nID = + menu.id == "tabContextMenu" + ? "tab-context-share-url" + : "menu-file-share-url"; + document.l10n.setAttributes(shareURL, l10nID); + + menu.insertBefore(shareURL, insertAfterEl.nextSibling); + return shareURL; + }, + + /** + * Returns a menu item specifically for accessing Windows sharing services. + */ + _buildShareURLItem() { + let shareURLMenuItem = document.createXULElement("menuitem"); + shareURLMenuItem.addEventListener("command", this); + return shareURLMenuItem; + }, + + /** + * Returns a menu specifically for accessing macOSx sharing services . + */ + _buildShareURLMenu() { + let menu = document.createXULElement("menu"); + let menuPopup = document.createXULElement("menupopup"); + menuPopup.addEventListener("popupshowing", this); + menu.appendChild(menuPopup); + return menu; + }, + + /** + * Get the sharing data for a given DOM node. + */ + getDataToShare(node) { + let browser = node.browserToShare?.get(); + let urlToShare = null; + let titleToShare = null; + + if (browser) { + let maybeToShare = BrowserUtils.getShareableURL(browser.currentURI); + if (maybeToShare) { + urlToShare = maybeToShare; + titleToShare = browser.contentTitle; + } + } + return { urlToShare, titleToShare }; + }, + + /** + * Populates the "Share" menupopup on macOSx. + */ + initializeShareURLPopup(menuPopup) { + if (AppConstants.platform != "macosx") { + return; + } + + // Empty menupopup + while (menuPopup.firstChild) { + menuPopup.firstChild.remove(); + } + + let { urlToShare } = this.getDataToShare(menuPopup.parentNode); + + // If we can't share the current URL, we display the items disabled, + // but enable the "more..." item at the bottom, to allow the user to + // change sharing preferences in the system dialog. + let shouldEnable = !!urlToShare; + if (!urlToShare) { + // Fake it so we can ask the sharing service for services: + urlToShare = makeURI("https://mozilla.org/"); + } + + let sharingService = gBrowser.MacSharingService; + let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec; + let services = sharingService.getSharingProviders(currentURI); + + services.forEach(share => { + let item = document.createXULElement("menuitem"); + item.classList.add("menuitem-iconic"); + item.setAttribute("label", share.menuItemTitle); + item.setAttribute("share-name", share.name); + item.setAttribute("image", share.image); + if (!shouldEnable) { + item.setAttribute("disabled", "true"); + } + menuPopup.appendChild(item); + }); + menuPopup.appendChild(document.createXULElement("menuseparator")); + let moreItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(moreItem, "menu-share-more"); + moreItem.classList.add("menuitem-iconic", "share-more-button"); + menuPopup.appendChild(moreItem); + + menuPopup.addEventListener("command", this); + menuPopup.parentNode + .closest("menupopup") + .addEventListener("popuphiding", this); + menuPopup.setAttribute("data-initialized", true); + }, + + onShareURLCommand(event) { + // Only call sharing services for the "Share" menu item. These services + // are accessed from a submenu popup for MacOS or the "Share" menu item + // for Windows. Use .closest() as a hack to find either the item itself + // or a parent with the right class. + let target = event.target.closest(".share-tab-url-item"); + if (!target) { + return; + } + + // urlToShare/titleToShare may be null, in which case only the "more" + // item is enabled, so handle that case first: + if (event.target.classList.contains("share-more-button")) { + gBrowser.MacSharingService.openSharingPreferences(); + return; + } + + let { urlToShare, titleToShare } = this.getDataToShare(target); + let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec; + + if (AppConstants.platform == "win") { + WindowsUIUtils.shareUrl(currentURI, titleToShare); + return; + } + + // On macOSX platforms + let shareName = event.target.getAttribute("share-name"); + + if (shareName) { + gBrowser.MacSharingService.shareUrl(shareName, currentURI, titleToShare); + } + }, + + onPopupHiding(event) { + // We don't want to rebuild the contents of the "Share" menupopup if only its submenu is + // hidden. So bail if this isn't the top menupopup in the DOM tree: + if (event.target.parentNode.closest("menupopup")) { + return; + } + // Otherwise, clear its "data-initialized" attribute. + let menupopup = event.target.querySelector( + ".share-tab-url-item" + )?.menupopup; + menupopup?.removeAttribute("data-initialized"); + + event.target.removeEventListener("popuphiding", this); + }, + + onPopupShowing(event) { + if (!event.target.hasAttribute("data-initialized")) { + this.initializeShareURLPopup(event.target); + } + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "command": + this.onShareURLCommand(aEvent); + break; + case "popuphiding": + this.onPopupHiding(aEvent); + break; + case "popupshowing": + this.onPopupShowing(aEvent); + break; + } + }, +}; + +/** + * Opens a new tab with the userContextId specified as an attribute of + * sourceEvent. This attribute is propagated to the top level originAttributes + * living on the tab's docShell. + * + * @param event + * A click event on a userContext File Menu option + */ +function openNewUserContextTab(event) { + openTrustedLinkIn(BROWSER_NEW_TAB_URL, "tab", { + userContextId: parseInt(event.target.getAttribute("data-usercontextid")), + }); +} + +var XULBrowserWindow = { + // Stored Status, Link and Loading values + status: "", + defaultStatus: "", + overLink: "", + startTime: 0, + isBusy: false, + busyUI: false, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsIWebProgressListener2", + "nsISupportsWeakReference", + "nsIXULBrowserWindow", + ]), + + get stopCommand() { + delete this.stopCommand; + return (this.stopCommand = document.getElementById("Browser:Stop")); + }, + get reloadCommand() { + delete this.reloadCommand; + return (this.reloadCommand = document.getElementById("Browser:Reload")); + }, + get _elementsForTextBasedTypes() { + delete this._elementsForTextBasedTypes; + return (this._elementsForTextBasedTypes = [ + document.getElementById("pageStyleMenu"), + document.getElementById("context-viewpartialsource-selection"), + document.getElementById("context-print-selection"), + ]); + }, + get _elementsForFind() { + delete this._elementsForFind; + return (this._elementsForFind = [ + document.getElementById("cmd_find"), + document.getElementById("cmd_findAgain"), + document.getElementById("cmd_findPrevious"), + ]); + }, + get _elementsForViewSource() { + delete this._elementsForViewSource; + return (this._elementsForViewSource = [ + document.getElementById("context-viewsource"), + document.getElementById("View:PageSource"), + ]); + }, + get _menuItemForRepairTextEncoding() { + delete this._menuItemForRepairTextEncoding; + return (this._menuItemForRepairTextEncoding = document.getElementById( + "repair-text-encoding" + )); + }, + get _menuItemForTranslations() { + delete this._menuItemForTranslations; + return (this._menuItemForTranslations = + document.getElementById("cmd_translate")); + }, + + setDefaultStatus(status) { + this.defaultStatus = status; + StatusPanel.update(); + }, + + /** + * Tells the UI what link we are currently over. + * + * @param {String} url + * The URL of the link. + * @param {Object} [options] + * This is an extension of nsIXULBrowserWindow for JS callers, will be + * passed on to LinkTargetDisplay. + */ + setOverLink(url, options = undefined) { + if (url) { + url = Services.textToSubURI.unEscapeURIForUI(url); + + // Encode bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + url = url.replace( + /[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, + encodeURIComponent + ); + + if (UrlbarPrefs.get("trimURLs")) { + url = BrowserUIUtils.trimURL(url); + } + } + + this.overLink = url; + LinkTargetDisplay.update(options); + }, + + onEnterDOMFullscreen() { + // Clear the status panel. + this.status = ""; + this.setDefaultStatus(""); + this.setOverLink("", { hideStatusPanelImmediately: true }); + }, + + showTooltip(xDevPix, yDevPix, tooltip, direction, browser) { + if ( + Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService) + .getCurrentSession() + ) { + return; + } + + if (!document.hasFocus()) { + return; + } + + let elt = document.getElementById("remoteBrowserTooltip"); + elt.label = tooltip; + elt.style.direction = direction; + elt.openPopupAtScreen( + xDevPix / window.devicePixelRatio, + yDevPix / window.devicePixelRatio, + false, + null + ); + }, + + hideTooltip() { + let elt = document.getElementById("remoteBrowserTooltip"); + elt.hidePopup(); + }, + + getTabCount() { + return gBrowser.tabs.length; + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + // Do nothing. + }, + + onProgressChange64( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + return this.onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ); + }, + + // This function fires only for the currently selected tab. + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + const nsIWebProgressListener = Ci.nsIWebProgressListener; + + let browser = gBrowser.selectedBrowser; + gProtectionsHandler.onStateChange(aWebProgress, aStateFlags); + + if ( + aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK + ) { + if (aRequest && aWebProgress.isTopLevel) { + // clear out search-engine data + browser.engines = null; + } + + this.isBusy = true; + + if ( + !(aStateFlags & nsIWebProgressListener.STATE_RESTORING) && + aWebProgress.isTopLevel + ) { + this.busyUI = true; + + // XXX: This needs to be based on window activity... + this.stopCommand.removeAttribute("disabled"); + CombinedStopReload.switchToStop(aRequest, aWebProgress); + } + } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + // This (thanks to the filter) is a network stop or the last + // request stop outside of loading the document, stop throbbers + // and progress bars and such + if (aRequest) { + let msg = ""; + let location; + let canViewSource = true; + // Get the URI either from a channel or a pseudo-object + if (aRequest instanceof Ci.nsIChannel || "URI" in aRequest) { + location = aRequest.URI; + + // For keyword URIs clear the user typed value since they will be changed into real URIs + if (location.scheme == "keyword" && aWebProgress.isTopLevel) { + gBrowser.userTypedValue = null; + } + + canViewSource = location.scheme != "view-source"; + + if (location.spec != "about:blank") { + switch (aStatus) { + case Cr.NS_ERROR_NET_TIMEOUT: + msg = gNavigatorBundle.getString("nv_timeout"); + break; + } + } + } + + this.status = ""; + this.setDefaultStatus(msg); + + // Disable View Source menu entries for images, enable otherwise + let isText = + browser.documentContentType && + BrowserUtils.mimeTypeIsTextBased(browser.documentContentType); + for (let element of this._elementsForViewSource) { + if (canViewSource && isText) { + element.removeAttribute("disabled"); + } else { + element.setAttribute("disabled", "true"); + } + } + + this._updateElementsForContentType(); + + // Update Override Text Encoding state. + // Can't cache the button, because the presence of the element in the DOM + // may change over time. + let button = document.getElementById("characterencoding-button"); + if (browser.mayEnableCharacterEncodingMenu) { + this._menuItemForRepairTextEncoding.removeAttribute("disabled"); + button?.removeAttribute("disabled"); + } else { + this._menuItemForRepairTextEncoding.setAttribute("disabled", "true"); + button?.setAttribute("disabled", "true"); + } + } + + this.isBusy = false; + + if (this.busyUI && aWebProgress.isTopLevel) { + this.busyUI = false; + + this.stopCommand.setAttribute("disabled", "true"); + CombinedStopReload.switchToReload(aRequest, aWebProgress); + } + } + }, + + /** + * An nsIWebProgressListener method called by tabbrowser. The `aIsSimulated` + * parameter is extra and not declared in nsIWebProgressListener, however; see + * below. + * + * @param {nsIWebProgress} aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param {nsIRequest} aRequest + * The associated nsIRequest. This may be null in some cases. + * @param {nsIURI} aLocationURI + * The URI of the location that is being loaded. + * @param {integer} aFlags + * Flags that indicate the reason the location changed. See the + * nsIWebProgressListener.LOCATION_CHANGE_* values. + * @param {boolean} aIsSimulated + * True when this is called by tabbrowser due to switching tabs and + * undefined otherwise. This parameter is not declared in + * nsIWebProgressListener.onLocationChange; see bug 1478348. + */ + onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags, aIsSimulated) { + var location = aLocationURI ? aLocationURI.spec : ""; + + UpdateBackForwardCommands(gBrowser.webNavigation); + + Services.obs.notifyObservers( + aWebProgress, + "touchbar-location-change", + location + ); + + // For most changes we only need to update the browser UI if the primary + // content area was navigated or the selected tab was changed. We don't need + // to do anything else if there was a subframe navigation. + + if (!aWebProgress.isTopLevel) { + return; + } + + this.setOverLink("", { hideStatusPanelImmediately: true }); + + let isSameDocument = + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT; + if ( + (location == "about:blank" && + BrowserUIUtils.checkEmptyPageOrigin(gBrowser.selectedBrowser)) || + location == "" + ) { + // Second condition is for new tabs, otherwise + // reload function is enabled until tab is refreshed. + this.reloadCommand.setAttribute("disabled", "true"); + } else { + this.reloadCommand.removeAttribute("disabled"); + } + + let isSessionRestore = !!( + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SESSION_STORE + ); + + // We want to update the popup visibility if we received this notification + // via simulated locationchange events such as switching between tabs, however + // if this is a document navigation then PopupNotifications will be updated + // via TabsProgressListener.onLocationChange and we do not want it called twice + gURLBar.setURI( + aLocationURI, + aIsSimulated, + isSessionRestore, + false, + isSameDocument + ); + + BookmarkingUI.onLocationChange(); + // If we've actually changed document, update the toolbar visibility. + if (!isSameDocument) { + updateBookmarkToolbarVisibility(); + } + + let closeOpenPanels = selector => { + for (let panel of document.querySelectorAll(selector)) { + if (panel.state != "closed") { + panel.hidePopup(); + } + } + }; + + // If the location is changed due to switching tabs, + // ensure we close any open tabspecific panels. + if (aIsSimulated) { + closeOpenPanels("panel[tabspecific='true']"); + } + + // Ensure we close any remaining open locationspecific panels + if (!isSameDocument) { + closeOpenPanels("panel[locationspecific='true']"); + } + + let screenshotsButtonsDisabled = + gScreenshots.shouldScreenshotsButtonBeDisabled(); + Services.obs.notifyObservers( + window, + "toggle-screenshot-disable", + screenshotsButtonsDisabled + ); + + gPermissionPanel.onLocationChange(); + + gProtectionsHandler.onLocationChange(); + + BrowserPageActions.onLocationChange(); + + SafeBrowsingNotificationBox.onLocationChange(aLocationURI); + + SaveToPocket.onLocationChange(window); + + let originalURI; + if (aRequest instanceof Ci.nsIChannel) { + originalURI = aRequest.originalURI; + } + + UrlbarProviderSearchTips.onLocationChange( + window, + aLocationURI, + originalURI, + aWebProgress, + aFlags + ); + + gTabletModePageCounter.inc(); + + this._updateElementsForContentType(); + + this._updateMacUserActivity(window, aLocationURI, aWebProgress); + + // Unconditionally disable the Text Encoding button during load to + // keep the UI calm when navigating from one modern page to another and + // the toolbar button is visible. + // Can't cache the button, because the presence of the element in the DOM + // may change over time. + let button = document.getElementById("characterencoding-button"); + this._menuItemForRepairTextEncoding.setAttribute("disabled", "true"); + button?.setAttribute("disabled", "true"); + + // Try not to instantiate gCustomizeMode as much as possible, + // so don't use CustomizeMode.sys.mjs to check for URI or customizing. + if ( + location == "about:blank" && + gBrowser.selectedTab.hasAttribute("customizemode") + ) { + gCustomizeMode.enter(); + } else if ( + CustomizationHandler.isEnteringCustomizeMode || + CustomizationHandler.isCustomizing() + ) { + gCustomizeMode.exit(); + } + + CFRPageActions.updatePageActions(gBrowser.selectedBrowser); + + AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser); + TranslationsParent.onLocationChange(gBrowser.selectedBrowser); + + PictureInPicture.updateUrlbarToggle(gBrowser.selectedBrowser); + + if (!gMultiProcessBrowser) { + // Bug 1108553 - Cannot rotate images with e10s + gGestureSupport.restoreRotationState(); + } + + // See bug 358202, when tabs are switched during a drag operation, + // timers don't fire on windows (bug 203573) + if (aRequest) { + setTimeout(function () { + XULBrowserWindow.asyncUpdateUI(); + }, 0); + } else { + this.asyncUpdateUI(); + } + + if (AppConstants.MOZ_CRASHREPORTER && aLocationURI) { + let uri = aLocationURI; + try { + // If the current URI contains a username/password, remove it. + uri = aLocationURI.mutate().setUserPass("").finalize(); + } catch (ex) { + /* Ignore failures on about: URIs. */ + } + + try { + Services.appinfo.annotateCrashReport("URL", uri.spec); + } catch (ex) { + // Don't make noise when the crash reporter is built but not enabled. + if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) { + throw ex; + } + } + } + }, + + _updateElementsForContentType() { + let browser = gBrowser.selectedBrowser; + + let isText = + browser.documentContentType && + BrowserUtils.mimeTypeIsTextBased(browser.documentContentType); + for (let element of this._elementsForTextBasedTypes) { + if (isText) { + element.removeAttribute("disabled"); + } else { + element.setAttribute("disabled", "true"); + } + } + + // Always enable find commands in PDF documents, otherwise do it only for + // text documents whose location is not in the blacklist. + let enableFind = + browser.contentPrincipal?.spec == "resource://pdf.js/web/viewer.html" || + (isText && BrowserUtils.canFindInPage(gBrowser.currentURI.spec)); + for (let element of this._elementsForFind) { + if (enableFind) { + element.removeAttribute("disabled"); + } else { + element.setAttribute("disabled", "true"); + } + } + + if (TranslationsParent.isRestrictedPage(gBrowser)) { + this._menuItemForTranslations.setAttribute("disabled", "true"); + } else { + this._menuItemForTranslations.removeAttribute("disabled"); + } + if (gTranslationsEnabled) { + if (TranslationsParent.getIsTranslationsEngineSupported()) { + this._menuItemForTranslations.removeAttribute("hidden"); + } else { + this._menuItemForTranslations.setAttribute("hidden", "true"); + } + } else { + this._menuItemForTranslations.setAttribute("hidden", "true"); + } + }, + + /** + * Updates macOS platform code with the current URI and page title. + * From there, we update the current NSUserActivity, enabling Handoff to other + * Apple devices. + * @param {Window} window + * The window in which the navigation occurred. + * @param {nsIURI} uri + * The URI pointing to the current page. + * @param {nsIWebProgress} webProgress + * The nsIWebProgress instance that fired a onLocationChange notification. + */ + _updateMacUserActivity(win, uri, webProgress) { + if (!webProgress.isTopLevel || AppConstants.platform != "macosx") { + return; + } + + let url = uri.spec; + if (PrivateBrowsingUtils.isWindowPrivate(win)) { + // Passing an empty string to MacUserActivityUpdater will invalidate the + // current user activity. + url = ""; + } + let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow); + MacUserActivityUpdater.updateLocation( + url, + win.gBrowser.contentTitle, + baseWin + ); + }, + + /** + * Potentially gets a URI for a MozBrowser to be shown to the user in the + * identity panel. For browsers whose content does not have a principal, + * this tries the precursor. If this is null, we should not override the + * browser's currentURI. + * @param {MozBrowser} browser + * The browser that we need a URI to show the user in the + * identity panel. + * @return nsIURI of the principal for the browser's content if + * the browser's currentURI should not be used, null otherwise. + */ + _securityURIOverride(browser) { + let uri = browser.currentURI; + if (!uri) { + return null; + } + + // If the browser's currentURI is sufficiently good that we + // do not require an override, bail out here. + // browser.currentURI should be used. + let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; + if ( + !(doGetProtocolFlags(uri) & URI_INHERITS_SECURITY_CONTEXT) && + !(uri.scheme == "about" && uri.filePath == "srcdoc") && + !(uri.scheme == "about" && uri.filePath == "blank") + ) { + return null; + } + + let principal = browser.contentPrincipal; + + if (principal.isNullPrincipal) { + principal = principal.precursorPrincipal; + } + + if (!principal) { + return null; + } + + // Can't get the original URI for a PDF viewer principal yet. + if (principal.originNoSuffix == "resource://pdf.js") { + return null; + } + + return principal.URI; + }, + + asyncUpdateUI() { + BrowserSearch.updateOpenSearchBadge(); + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + this.status = aMessage; + StatusPanel.update(); + }, + + // Properties used to cache security state used to update the UI + _state: null, + _lastLocation: null, + _event: null, + _lastLocationForEvent: null, + // _isSecureContext can change without the state/location changing, due to security + // error pages that intercept certain loads. For example this happens sometimes + // with the the HTTPS-Only Mode error page (more details in bug 1656027) + _isSecureContext: null, + + // This is called in multiple ways: + // 1. Due to the nsIWebProgressListener.onContentBlockingEvent notification. + // 2. Called by tabbrowser.xml when updating the current browser. + // 3. Called directly during this object's initializations. + // 4. Due to the nsIWebProgressListener.onLocationChange notification. + // aRequest will be null always in case 2 and 3, and sometimes in case 1 (for + // instance, there won't be a request when STATE_BLOCKED_TRACKING_CONTENT or + // other blocking events are observed). + onContentBlockingEvent(aWebProgress, aRequest, aEvent, aIsSimulated) { + // Don't need to do anything if the data we use to update the UI hasn't + // changed + let uri = gBrowser.currentURI; + let spec = uri.spec; + if (this._event == aEvent && this._lastLocationForEvent == spec) { + return; + } + this._lastLocationForEvent = spec; + + if ( + typeof aIsSimulated != "boolean" && + typeof aIsSimulated != "undefined" + ) { + throw new Error( + "onContentBlockingEvent: aIsSimulated receieved an unexpected type" + ); + } + + gProtectionsHandler.onContentBlockingEvent( + aEvent, + aWebProgress, + aIsSimulated, + this._event // previous content blocking event + ); + + // We need the state of the previous content blocking event, so update + // event after onContentBlockingEvent is called. + this._event = aEvent; + }, + + // This is called in multiple ways: + // 1. Due to the nsIWebProgressListener.onSecurityChange notification. + // 2. Called by tabbrowser.xml when updating the current browser. + // 3. Called directly during this object's initializations. + // aRequest will be null always in case 2 and 3, and sometimes in case 1. + onSecurityChange(aWebProgress, aRequest, aState, aIsSimulated) { + // Don't need to do anything if the data we use to update the UI hasn't + // changed + let uri = gBrowser.currentURI; + let spec = uri.spec; + let isSecureContext = gBrowser.securityUI.isSecureContext; + if ( + this._state == aState && + this._lastLocation == spec && + this._isSecureContext === isSecureContext + ) { + // Switching to a tab of the same URL doesn't change most security + // information, but tab specific permissions may be different. + gIdentityHandler.refreshIdentityBlock(); + return; + } + this._state = aState; + this._lastLocation = spec; + this._isSecureContext = isSecureContext; + + // Make sure the "https" part of the URL is striked out or not, + // depending on the current mixed active content blocking state. + gURLBar.formatValue(); + + // Update the identity panel, making sure we use the precursorPrincipal's + // URI where appropriate, for example about:blank windows. + let uriOverride = this._securityURIOverride(gBrowser.selectedBrowser); + if (uriOverride) { + uri = uriOverride; + this._state |= Ci.nsIWebProgressListener.STATE_IDENTITY_ASSOCIATED; + } + + try { + uri = Services.io.createExposableURI(uri); + } catch (e) {} + gIdentityHandler.updateIdentity(this._state, uri); + }, + + // simulate all change notifications after switching tabs + onUpdateCurrentBrowser: function XWB_onUpdateCurrentBrowser( + aStateFlags, + aStatus, + aMessage, + aTotalProgress + ) { + if (FullZoom.updateBackgroundTabs) { + FullZoom.onLocationChange(gBrowser.currentURI, true); + } + + CombinedStopReload.onTabSwitch(); + + // Docshell should normally take care of hiding the tooltip, but we need to do it + // ourselves for tabswitches. + this.hideTooltip(); + + // Also hide tooltips for content loaded in the parent process: + document.getElementById("aHTMLTooltip").hidePopup(); + + var nsIWebProgressListener = Ci.nsIWebProgressListener; + var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP; + // use a pseudo-object instead of a (potentially nonexistent) channel for getting + // a correct error message - and make sure that the UI is always either in + // loading (STATE_START) or done (STATE_STOP) mode + this.onStateChange( + gBrowser.webProgress, + { URI: gBrowser.currentURI }, + loadingDone + ? nsIWebProgressListener.STATE_STOP + : nsIWebProgressListener.STATE_START, + aStatus + ); + // status message and progress value are undefined if we're done with loading + if (loadingDone) { + return; + } + this.onStatusChange(gBrowser.webProgress, null, 0, aMessage); + }, +}; + +var LinkTargetDisplay = { + get DELAY_SHOW() { + delete this.DELAY_SHOW; + return (this.DELAY_SHOW = Services.prefs.getIntPref( + "browser.overlink-delay" + )); + }, + + DELAY_HIDE: 250, + _timer: 0, + + get _contextMenu() { + delete this._contextMenu; + return (this._contextMenu = document.getElementById( + "contentAreaContextMenu" + )); + }, + + update({ hideStatusPanelImmediately = false } = {}) { + if ( + this._contextMenu.state == "open" || + this._contextMenu.state == "showing" + ) { + this._contextMenu.addEventListener("popuphidden", () => this.update(), { + once: true, + }); + return; + } + + clearTimeout(this._timer); + window.removeEventListener("mousemove", this, true); + + if (!XULBrowserWindow.overLink) { + if (hideStatusPanelImmediately) { + this._hide(); + } else { + this._timer = setTimeout(this._hide.bind(this), this.DELAY_HIDE); + } + return; + } + + if (StatusPanel.isVisible) { + StatusPanel.update(); + } else { + // Let the display appear when the mouse doesn't move within the delay + this._showDelayed(); + window.addEventListener("mousemove", this, true); + } + }, + + handleEvent(event) { + switch (event.type) { + case "mousemove": + // Restart the delay since the mouse was moved + clearTimeout(this._timer); + this._showDelayed(); + break; + } + }, + + _showDelayed() { + this._timer = setTimeout( + function (self) { + StatusPanel.update(); + window.removeEventListener("mousemove", self, true); + }, + this.DELAY_SHOW, + this + ); + }, + + _hide() { + clearTimeout(this._timer); + + StatusPanel.update(); + }, +}; + +var CombinedStopReload = { + // Try to initialize. Returns whether initialization was successful, which + // may mean we had already initialized. + ensureInitialized() { + if (this._initialized) { + return true; + } + if (this._destroyed) { + return false; + } + + let reload = document.getElementById("reload-button"); + let stop = document.getElementById("stop-button"); + // It's possible the stop/reload buttons have been moved to the palette. + // They may be reinserted later, so we will retry initialization if/when + // we get notified of document loads. + if (!stop || !reload) { + return false; + } + + this._initialized = true; + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") { + reload.setAttribute("displaystop", "true"); + } + stop.addEventListener("click", this); + + // Removing attributes based on the observed command doesn't happen if the button + // is in the palette when the command's attribute is removed (cf. bug 309953) + for (let button of [stop, reload]) { + if (button.hasAttribute("disabled")) { + let command = document.getElementById(button.getAttribute("command")); + if (!command.hasAttribute("disabled")) { + button.removeAttribute("disabled"); + } + } + } + + this.reload = reload; + this.stop = stop; + this.stopReloadContainer = this.reload.parentNode; + this.timeWhenSwitchedToStop = 0; + + this.stopReloadContainer.addEventListener("animationend", this); + this.stopReloadContainer.addEventListener("animationcancel", this); + + return true; + }, + + uninit() { + this._destroyed = true; + + if (!this._initialized) { + return; + } + + this._cancelTransition(); + this.stop.removeEventListener("click", this); + this.stopReloadContainer.removeEventListener("animationend", this); + this.stopReloadContainer.removeEventListener("animationcancel", this); + this.stopReloadContainer = null; + this.reload = null; + this.stop = null; + }, + + handleEvent(event) { + switch (event.type) { + case "click": + if (event.button == 0 && !this.stop.disabled) { + this._stopClicked = true; + } + break; + case "animationcancel": + case "animationend": { + if ( + event.target.classList.contains("toolbarbutton-animatable-image") && + (event.animationName == "reload-to-stop" || + event.animationName == "stop-to-reload") + ) { + this.stopReloadContainer.removeAttribute("animate"); + } + } + } + }, + + onTabSwitch() { + // Reset the time in the event of a tabswitch since the stored time + // would have been associated with the previous tab, so the animation will + // still run if the page has been loading until long after the tab switch. + this.timeWhenSwitchedToStop = window.performance.now(); + }, + + switchToStop(aRequest, aWebProgress) { + if ( + !this.ensureInitialized() || + !this._shouldSwitch(aRequest, aWebProgress) + ) { + return; + } + + // Store the time that we switched to the stop button only if a request + // is active. Requests are null if the switch is related to a tabswitch. + // This is used to determine if we should show the stop->reload animation. + if (aRequest instanceof Ci.nsIRequest) { + this.timeWhenSwitchedToStop = window.performance.now(); + } + + let shouldAnimate = + aRequest instanceof Ci.nsIRequest && + aWebProgress.isTopLevel && + aWebProgress.isLoadingDocument && + !gBrowser.tabAnimationsInProgress && + !gReduceMotion && + this.stopReloadContainer.closest("#nav-bar-customization-target"); + + this._cancelTransition(); + if (shouldAnimate) { + this.stopReloadContainer.setAttribute("animate", "true"); + } else { + this.stopReloadContainer.removeAttribute("animate"); + } + this.reload.setAttribute("displaystop", "true"); + }, + + switchToReload(aRequest, aWebProgress) { + if (!this.ensureInitialized() || !this.reload.hasAttribute("displaystop")) { + return; + } + + let shouldAnimate = + aRequest instanceof Ci.nsIRequest && + aWebProgress.isTopLevel && + !aWebProgress.isLoadingDocument && + !gBrowser.tabAnimationsInProgress && + !gReduceMotion && + this._loadTimeExceedsMinimumForAnimation() && + this.stopReloadContainer.closest("#nav-bar-customization-target"); + + if (shouldAnimate) { + this.stopReloadContainer.setAttribute("animate", "true"); + } else { + this.stopReloadContainer.removeAttribute("animate"); + } + + this.reload.removeAttribute("displaystop"); + + if (!shouldAnimate || this._stopClicked) { + this._stopClicked = false; + this._cancelTransition(); + this.reload.disabled = + XULBrowserWindow.reloadCommand.getAttribute("disabled") == "true"; + return; + } + + if (this._timer) { + return; + } + + // Temporarily disable the reload button to prevent the user from + // accidentally reloading the page when intending to click the stop button + this.reload.disabled = true; + this._timer = setTimeout( + function (self) { + self._timer = 0; + self.reload.disabled = + XULBrowserWindow.reloadCommand.getAttribute("disabled") == "true"; + }, + 650, + this + ); + }, + + _loadTimeExceedsMinimumForAnimation() { + // If the time between switching to the stop button then switching to + // the reload button exceeds 150ms, then we will show the animation. + // If we don't know when we switched to stop (switchToStop is called + // after init but before switchToReload), then we will prevent the + // animation from occuring. + return ( + this.timeWhenSwitchedToStop && + window.performance.now() - this.timeWhenSwitchedToStop > 150 + ); + }, + + _shouldSwitch(aRequest, aWebProgress) { + if ( + aRequest && + aRequest.originalURI && + (aRequest.originalURI.schemeIs("chrome") || + (aRequest.originalURI.schemeIs("about") && + aWebProgress.isTopLevel && + !aRequest.originalURI.spec.startsWith("about:reader"))) + ) { + return false; + } + + return true; + }, + + _cancelTransition() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = 0; + } + }, +}; + +var TabsProgressListener = { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + // Collect telemetry data about tab load times. + if ( + aWebProgress.isTopLevel && + (!aRequest.originalURI || aRequest.originalURI.scheme != "about") + ) { + let histogram = "FX_PAGE_LOAD_MS_2"; + let recordLoadTelemetry = true; + + if (aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) { + // loadType is constructed by shifting loadFlags, this is why we need to + // do the same shifting here. + // https://searchfox.org/mozilla-central/rev/11cfa0462a6b5d8c5e2111b8cfddcf78098f0141/docshell/base/nsDocShellLoadTypes.h#22 + if (aWebProgress.loadType & (kSkipCacheFlags << 16)) { + histogram = "FX_PAGE_RELOAD_SKIP_CACHE_MS"; + } else if (aWebProgress.loadType == Ci.nsIDocShell.LOAD_CMD_RELOAD) { + histogram = "FX_PAGE_RELOAD_NORMAL_MS"; + } else { + recordLoadTelemetry = false; + } + } + + let stopwatchRunning = TelemetryStopwatch.running(histogram, aBrowser); + if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + if (stopwatchRunning) { + // Oops, we're seeing another start without having noticed the previous stop. + if (recordLoadTelemetry) { + TelemetryStopwatch.cancel(histogram, aBrowser); + } + } + if (recordLoadTelemetry) { + TelemetryStopwatch.start(histogram, aBrowser); + } + Services.telemetry.getHistogramById("FX_TOTAL_TOP_VISITS").add(true); + } else if ( + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + stopwatchRunning /* we won't see STATE_START events for pre-rendered tabs */ + ) { + if (recordLoadTelemetry) { + TelemetryStopwatch.finish(histogram, aBrowser); + BrowserTelemetryUtils.recordSiteOriginTelemetry(browserWindows()); + } + } + } else if ( + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStatus == Cr.NS_BINDING_ABORTED && + stopwatchRunning /* we won't see STATE_START events for pre-rendered tabs */ + ) { + if (recordLoadTelemetry) { + TelemetryStopwatch.cancel(histogram, aBrowser); + } + } + } + }, + + onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) { + // Filter out location changes in sub documents. + if (!aWebProgress.isTopLevel) { + return; + } + + // Some shops use pushState to move between individual products, so + // the shopping code needs to be told about all of these. + ShoppingSidebarManager.onLocationChange(aBrowser, aLocationURI, aFlags); + + // Filter out location changes caused by anchor navigation + // or history.push/pop/replaceState. + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { + // Reader mode cares about history.pushState and friends. + // FIXME: The content process should manage this directly (bug 1445351). + aBrowser.sendMessageToActor( + "Reader:PushState", + { + isArticle: aBrowser.isArticle, + }, + "AboutReader" + ); + return; + } + + // Only need to call locationChange if the PopupNotifications object + // for this window has already been initialized (i.e. its getter no + // longer exists) + if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) { + PopupNotifications.locationChange(aBrowser); + } + + let tab = gBrowser.getTabForBrowser(aBrowser); + if (tab && tab._sharingState) { + gBrowser.resetBrowserSharing(aBrowser); + } + + gBrowser.readNotificationBox(aBrowser)?.removeTransientNotifications(); + + FullZoom.onLocationChange(aLocationURI, false, aBrowser); + CaptivePortalWatcher.onLocationChange(aBrowser); + }, + + onLinkIconAvailable(browser, dataURI, iconURI) { + if (!iconURI) { + return; + } + if (browser == gBrowser.selectedBrowser) { + // If the "Add Search Engine" page action is in the urlbar, its image + // needs to be set to the new icon, so call updateOpenSearchBadge. + BrowserSearch.updateOpenSearchBadge(); + } + }, +}; + +function nsBrowserAccess() {} + +nsBrowserAccess.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIBrowserDOMWindow"]), + + _openURIInNewTab( + aURI, + aReferrerInfo, + aIsPrivate, + aIsExternal, + aForceNotRemote = false, + aUserContextId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, + aOpenWindowInfo = null, + aOpenerBrowser = null, + aTriggeringPrincipal = null, + aName = "", + aCsp = null, + aSkipLoad = false, + aForceLoadInBackground = false + ) { + let win, needToFocusWin; + + // try the current window. if we're in a popup, fall back on the most recent browser window + if (window.toolbar.visible) { + win = window; + } else { + win = BrowserWindowTracker.getTopWindow({ private: aIsPrivate }); + needToFocusWin = true; + } + + if (!win) { + // we couldn't find a suitable window, a new one needs to be opened. + return null; + } + + if (aIsExternal && (!aURI || aURI.spec == "about:blank")) { + win.BrowserOpenTab(); // this also focuses the location bar + win.focus(); + return win.gBrowser.selectedBrowser; + } + + let loadInBackground = Services.prefs.getBoolPref( + "browser.tabs.loadDivertedInBackground" + ); + if (aForceLoadInBackground) { + loadInBackground = true; + } + + let tab = win.gBrowser.addTab(aURI ? aURI.spec : "about:blank", { + triggeringPrincipal: aTriggeringPrincipal, + referrerInfo: aReferrerInfo, + userContextId: aUserContextId, + fromExternal: aIsExternal, + inBackground: loadInBackground, + forceNotRemote: aForceNotRemote, + openWindowInfo: aOpenWindowInfo, + openerBrowser: aOpenerBrowser, + name: aName, + csp: aCsp, + skipLoad: aSkipLoad, + }); + let browser = win.gBrowser.getBrowserForTab(tab); + + if (needToFocusWin || (!loadInBackground && aIsExternal)) { + win.focus(); + } + + return browser; + }, + + createContentWindow( + aURI, + aOpenWindowInfo, + aWhere, + aFlags, + aTriggeringPrincipal, + aCsp + ) { + return this.getContentWindowOrOpenURI( + null, + aOpenWindowInfo, + aWhere, + aFlags, + aTriggeringPrincipal, + aCsp, + true + ); + }, + + openURI(aURI, aOpenWindowInfo, aWhere, aFlags, aTriggeringPrincipal, aCsp) { + if (!aURI) { + console.error("openURI should only be called with a valid URI"); + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + return this.getContentWindowOrOpenURI( + aURI, + aOpenWindowInfo, + aWhere, + aFlags, + aTriggeringPrincipal, + aCsp, + false + ); + }, + + getContentWindowOrOpenURI( + aURI, + aOpenWindowInfo, + aWhere, + aFlags, + aTriggeringPrincipal, + aCsp, + aSkipLoad + ) { + var browsingContext = null; + var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + var guessUserContextIdEnabled = + isExternal && + !Services.prefs.getBoolPref( + "browser.link.force_default_user_context_id_for_external_opens", + false + ); + var openingUserContextId = + (guessUserContextIdEnabled && + URILoadingHelper.guessUserContextId(aURI)) || + Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + + if (aOpenWindowInfo && isExternal) { + console.error( + "nsBrowserAccess.openURI did not expect aOpenWindowInfo to be " + + "passed if the context is OPEN_EXTERNAL." + ); + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + if (isExternal && aURI && aURI.schemeIs("chrome")) { + dump("use --chrome command-line option to load external chrome urls\n"); + return null; + } + + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { + if ( + isExternal && + Services.prefs.prefHasUserValue( + "browser.link.open_newwindow.override.external" + ) + ) { + aWhere = Services.prefs.getIntPref( + "browser.link.open_newwindow.override.external" + ); + } else { + aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); + } + } + + let referrerInfo; + if (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_REFERRER) { + referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, false, null); + } else if ( + aOpenWindowInfo && + aOpenWindowInfo.parent && + aOpenWindowInfo.parent.window + ) { + referrerInfo = new ReferrerInfo( + aOpenWindowInfo.parent.window.document.referrerInfo.referrerPolicy, + true, + makeURI(aOpenWindowInfo.parent.window.location.href) + ); + } else { + referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, null); + } + + let isPrivate = aOpenWindowInfo + ? aOpenWindowInfo.originAttributes.privateBrowsingId != 0 + : PrivateBrowsingUtils.isWindowPrivate(window); + + switch (aWhere) { + case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW: + // FIXME: Bug 408379. So how come this doesn't send the + // referrer like the other loads do? + var url = aURI && aURI.spec; + let features = "all,dialog=no"; + if (isPrivate) { + features += ",private"; + } + // Pass all params to openDialog to ensure that "url" isn't passed through + // loadOneOrMoreURIs, which splits based on "|" + try { + let extraOptions = Cc[ + "@mozilla.org/hash-property-bag;1" + ].createInstance(Ci.nsIWritablePropertyBag2); + extraOptions.setPropertyAsBool("fromExternal", isExternal); + + openDialog( + AppConstants.BROWSER_CHROME_URL, + "_blank", + features, + // window.arguments + url, + extraOptions, + null, + null, + null, + null, + null, + null, + aTriggeringPrincipal, + null, + aCsp, + aOpenWindowInfo + ); + // At this point, the new browser window is just starting to load, and + // hasn't created the content <browser> that we should return. + // If the caller of this function is originating in C++, they can pass a + // callback in nsOpenWindowInfo and it will be invoked when the browsing + // context for a newly opened window is ready. + browsingContext = null; + } catch (ex) { + console.error(ex); + } + break; + case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB: + case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND: { + // If we have an opener, that means that the caller is expecting access + // to the nsIDOMWindow of the opened tab right away. For e10s windows, + // this means forcing the newly opened browser to be non-remote so that + // we can hand back the nsIDOMWindow. DocumentLoadListener will do the + // job of shuttling off the newly opened browser to run in the right + // process once it starts loading a URI. + let forceNotRemote = aOpenWindowInfo && !aOpenWindowInfo.isRemote; + let userContextId = aOpenWindowInfo + ? aOpenWindowInfo.originAttributes.userContextId + : openingUserContextId; + let forceLoadInBackground = + aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND; + let browser = this._openURIInNewTab( + aURI, + referrerInfo, + isPrivate, + isExternal, + forceNotRemote, + userContextId, + aOpenWindowInfo, + aOpenWindowInfo?.parent?.top.embedderElement, + aTriggeringPrincipal, + "", + aCsp, + aSkipLoad, + forceLoadInBackground + ); + if (browser) { + browsingContext = browser.browsingContext; + } + break; + } + case Ci.nsIBrowserDOMWindow.OPEN_PRINT_BROWSER: { + let browser = + PrintUtils.handleStaticCloneCreatedForPrint(aOpenWindowInfo); + if (browser) { + browsingContext = browser.browsingContext; + } + break; + } + default: + // OPEN_CURRENTWINDOW or an illegal value + browsingContext = window.gBrowser.selectedBrowser.browsingContext; + if (aURI) { + let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (isExternal) { + loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + } else if (!aTriggeringPrincipal.isSystemPrincipal) { + // XXX this code must be reviewed and changed when bug 1616353 + // lands. + loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIRST_LOAD; + } + // This should ideally be able to call loadURI with the actual URI. + // However, that would bypass some styles of fixup (notably Windows + // paths passed as "URI"s), so this needs some further thought. It + // should be addressed in bug 1815509. + gBrowser.fixupAndLoadURIString(aURI.spec, { + triggeringPrincipal: aTriggeringPrincipal, + csp: aCsp, + loadFlags, + referrerInfo, + }); + } + if ( + !Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground") + ) { + window.focus(); + } + } + return browsingContext; + }, + + createContentWindowInFrame: function browser_createContentWindowInFrame( + aURI, + aParams, + aWhere, + aFlags, + aName + ) { + // Passing a null-URI to only create the content window, + // and pass true for aSkipLoad to prevent loading of + // about:blank + return this.getContentWindowOrOpenURIInFrame( + null, + aParams, + aWhere, + aFlags, + aName, + true + ); + }, + + openURIInFrame: function browser_openURIInFrame( + aURI, + aParams, + aWhere, + aFlags, + aName + ) { + return this.getContentWindowOrOpenURIInFrame( + aURI, + aParams, + aWhere, + aFlags, + aName, + false + ); + }, + + getContentWindowOrOpenURIInFrame: + function browser_getContentWindowOrOpenURIInFrame( + aURI, + aParams, + aWhere, + aFlags, + aName, + aSkipLoad + ) { + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_PRINT_BROWSER) { + return PrintUtils.handleStaticCloneCreatedForPrint( + aParams.openWindowInfo + ); + } + + if ( + aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB && + aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND + ) { + dump("Error: openURIInFrame can only open in new tabs or print"); + return null; + } + + var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + var userContextId = + aParams.openerOriginAttributes && + "userContextId" in aParams.openerOriginAttributes + ? aParams.openerOriginAttributes.userContextId + : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + + var forceLoadInBackground = + aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND; + + return this._openURIInNewTab( + aURI, + aParams.referrerInfo, + aParams.isPrivate, + isExternal, + false, + userContextId, + aParams.openWindowInfo, + aParams.openerBrowser, + aParams.triggeringPrincipal, + aName, + aParams.csp, + aSkipLoad, + forceLoadInBackground + ); + }, + + canClose() { + return CanCloseWindow(); + }, + + get tabCount() { + return gBrowser.tabs.length; + }, +}; + +function showFullScreenViewContextMenuItems(popup) { + for (let node of popup.querySelectorAll('[contexttype="fullscreen"]')) { + node.hidden = !window.fullScreen; + } + let autoHide = popup.querySelector(".fullscreen-context-autohide"); + if (autoHide) { + FullScreen.updateAutohideMenuitem(autoHide); + } +} + +function onViewToolbarsPopupShowing(aEvent, aInsertPoint) { + var popup = aEvent.target; + if (popup != aEvent.currentTarget) { + return; + } + + // Empty the menu + for (var i = popup.children.length - 1; i >= 0; --i) { + var deadItem = popup.children[i]; + if (deadItem.hasAttribute("toolbarId")) { + popup.removeChild(deadItem); + } + } + + MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl"); + let firstMenuItem = aInsertPoint || popup.firstElementChild; + let toolbarNodes = gNavToolbox.querySelectorAll("toolbar"); + for (let toolbar of toolbarNodes) { + if (!toolbar.hasAttribute("toolbarname")) { + continue; + } + + if (toolbar.id == "PersonalToolbar") { + let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(toolbar); + popup.insertBefore(menu, firstMenuItem); + } else { + let menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("id", "toggle_" + toolbar.id); + menuItem.setAttribute("toolbarId", toolbar.id); + menuItem.setAttribute("type", "checkbox"); + menuItem.setAttribute("label", toolbar.getAttribute("toolbarname")); + let hidingAttribute = + toolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; + menuItem.setAttribute( + "checked", + toolbar.getAttribute(hidingAttribute) != "true" + ); + menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey")); + if (popup.id != "toolbar-context-menu") { + menuItem.setAttribute("key", toolbar.getAttribute("key")); + } + + popup.insertBefore(menuItem, firstMenuItem); + menuItem.addEventListener("command", onViewToolbarCommand); + } + } + + let moveToPanel = popup.querySelector(".customize-context-moveToPanel"); + let removeFromToolbar = popup.querySelector( + ".customize-context-removeFromToolbar" + ); + // Show/hide fullscreen context menu items and set the + // autohide item's checked state to mirror the autohide pref. + showFullScreenViewContextMenuItems(popup); + // View -> Toolbars menu doesn't have the moveToPanel or removeFromToolbar items. + if (!moveToPanel || !removeFromToolbar) { + return; + } + + // triggerNode can be a nested child element of a toolbaritem. + let toolbarItem = popup.triggerNode; + + if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") { + toolbarItem = toolbarItem.firstElementChild; + } else if (toolbarItem && toolbarItem.localName != "toolbar") { + while (toolbarItem && toolbarItem.parentElement) { + let parent = toolbarItem.parentElement; + if ( + (parent.classList && + parent.classList.contains("customization-target")) || + parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well. + parent.localName == "toolbarpaletteitem" || + parent.localName == "toolbar" + ) { + break; + } + toolbarItem = parent; + } + } else { + toolbarItem = null; + } + + let showTabStripItems = toolbarItem && toolbarItem.id == "tabbrowser-tabs"; + for (let node of popup.querySelectorAll( + 'menuitem[contexttype="toolbaritem"]' + )) { + node.hidden = showTabStripItems; + } + + for (let node of popup.querySelectorAll('menuitem[contexttype="tabbar"]')) { + node.hidden = !showTabStripItems; + } + + document + .getElementById("toolbar-context-menu") + .querySelectorAll("[data-lazy-l10n-id]") + .forEach(el => { + el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id")); + el.removeAttribute("data-lazy-l10n-id"); + }); + + // The "normal" toolbar items menu separator is hidden because it's unused + // when hiding the "moveToPanel" and "removeFromToolbar" items on flexible + // space items. But we need to ensure its hidden state is reset in the case + // the context menu is subsequently opened on a non-flexible space item. + let menuSeparator = document.getElementById("toolbarItemsMenuSeparator"); + menuSeparator.hidden = false; + + document.getElementById("toolbarNavigatorItemsMenuSeparator").hidden = + !showTabStripItems; + + if ( + !CustomizationHandler.isCustomizing() && + CustomizableUI.isSpecialWidget(toolbarItem?.id || "") + ) { + moveToPanel.hidden = true; + removeFromToolbar.hidden = true; + menuSeparator.hidden = !showTabStripItems; + } + + if (showTabStripItems) { + let multipleTabsSelected = !!gBrowser.multiSelectedTabsCount; + document.getElementById("toolbar-context-bookmarkSelectedTabs").hidden = + !multipleTabsSelected; + document.getElementById("toolbar-context-bookmarkSelectedTab").hidden = + multipleTabsSelected; + document.getElementById("toolbar-context-reloadSelectedTabs").hidden = + !multipleTabsSelected; + document.getElementById("toolbar-context-reloadSelectedTab").hidden = + multipleTabsSelected; + document.getElementById("toolbar-context-selectAllTabs").disabled = + gBrowser.allTabsSelected(); + document.getElementById("toolbar-context-undoCloseTab").disabled = + SessionStore.getClosedTabCount() == 0; + return; + } + + let movable = + toolbarItem && + toolbarItem.id && + CustomizableUI.isWidgetRemovable(toolbarItem); + if (movable) { + if (CustomizableUI.isSpecialWidget(toolbarItem.id)) { + moveToPanel.setAttribute("disabled", true); + } else { + moveToPanel.removeAttribute("disabled"); + } + removeFromToolbar.removeAttribute("disabled"); + } else { + moveToPanel.setAttribute("disabled", true); + removeFromToolbar.setAttribute("disabled", true); + } +} + +function onViewToolbarCommand(aEvent) { + let node = aEvent.originalTarget; + let menuId; + let toolbarId; + let isVisible; + if (node.dataset.bookmarksToolbarVisibility) { + isVisible = node.dataset.visibilityEnum; + toolbarId = "PersonalToolbar"; + menuId = node.parentNode.parentNode.parentNode.id; + Services.prefs.setCharPref( + "browser.toolbars.bookmarks.visibility", + isVisible + ); + } else { + menuId = node.parentNode.id; + toolbarId = node.getAttribute("toolbarId"); + isVisible = node.getAttribute("checked") == "true"; + } + CustomizableUI.setToolbarVisibility(toolbarId, isVisible); + BrowserUsageTelemetry.recordToolbarVisibility(toolbarId, isVisible, menuId); +} + +function setToolbarVisibility( + toolbar, + isVisible, + persist = true, + animated = true +) { + let hidingAttribute; + if (toolbar.getAttribute("type") == "menubar") { + hidingAttribute = "autohide"; + if (AppConstants.platform == "linux") { + Services.prefs.setBoolPref("ui.key.menuAccessKeyFocuses", !isVisible); + } + } else { + hidingAttribute = "collapsed"; + } + + if (toolbar == BookmarkingUI.toolbar) { + // For the bookmarks toolbar, we need to persist state before toggling + // the visibility in this window, because the state can be different + // (newtab vs never or always) even when that won't change visibility + // in this window. + if (persist) { + let prefValue; + if (typeof isVisible == "string") { + prefValue = isVisible; + } else { + prefValue = isVisible ? "always" : "never"; + } + Services.prefs.setCharPref( + "browser.toolbars.bookmarks.visibility", + prefValue + ); + } + + const overlapAttr = "BookmarksToolbarOverlapsBrowser"; + switch (isVisible) { + case true: + case "always": + isVisible = true; + document.documentElement.toggleAttribute(overlapAttr, false); + break; + case false: + case "never": + isVisible = false; + document.documentElement.toggleAttribute(overlapAttr, false); + break; + case "newtab": + default: + let currentURI = gBrowser?.currentURI; + if (!gBrowserInit.domContentLoaded) { + let uriToLoad = gBrowserInit.uriToLoadPromise; + if (uriToLoad) { + if (Array.isArray(uriToLoad)) { + // We only care about the first tab being loaded + uriToLoad = uriToLoad[0]; + } + try { + currentURI = Services.io.newURI(uriToLoad); + } catch (ex) {} + } + } + isVisible = BookmarkingUI.isOnNewTabPage(currentURI); + document.documentElement.toggleAttribute(overlapAttr, isVisible); + break; + } + } + + if (toolbar.getAttribute(hidingAttribute) == (!isVisible).toString()) { + // If this call will not result in a visibility change, return early + // since dispatching toolbarvisibilitychange will cause views to get rebuilt. + return; + } + + toolbar.classList.toggle("instant", !animated); + toolbar.setAttribute(hidingAttribute, !isVisible); + // For the bookmarks toolbar, we will have saved state above. For other + // toolbars, we need to do it after setting the attribute, or we might + // save the wrong state. + if (persist && toolbar.id != "PersonalToolbar") { + Services.xulStore.persist(toolbar, hidingAttribute); + } + + let eventParams = { + detail: { + visible: isVisible, + }, + bubbles: true, + }; + let event = new CustomEvent("toolbarvisibilitychange", eventParams); + toolbar.dispatchEvent(event); +} + +function updateToggleControlLabel(control) { + if (!control.hasAttribute("label-checked")) { + return; + } + + if (!control.hasAttribute("label-unchecked")) { + control.setAttribute("label-unchecked", control.getAttribute("label")); + } + let prefix = control.getAttribute("checked") == "true" ? "" : "un"; + control.setAttribute("label", control.getAttribute(`label-${prefix}checked`)); +} + +var TabletModeUpdater = { + init() { + if (AppConstants.platform == "win") { + this.update(WindowsUIUtils.inTabletMode); + Services.obs.addObserver(this, "tablet-mode-change"); + } + }, + + uninit() { + if (AppConstants.platform == "win") { + Services.obs.removeObserver(this, "tablet-mode-change"); + } + }, + + observe(subject, topic, data) { + this.update(data == "tablet-mode"); + }, + + update(isInTabletMode) { + let wasInTabletMode = document.documentElement.hasAttribute("tabletmode"); + if (isInTabletMode) { + document.documentElement.setAttribute("tabletmode", "true"); + } else { + document.documentElement.removeAttribute("tabletmode"); + } + if (wasInTabletMode != isInTabletMode) { + gUIDensity.update(); + } + }, +}; + +var gTabletModePageCounter = { + enabled: false, + inc() { + this.enabled = AppConstants.platform == "win"; + if (!this.enabled) { + this.inc = () => {}; + return; + } + this.inc = this._realInc; + this.inc(); + }, + + _desktopCount: 0, + _tabletCount: 0, + _realInc() { + let inTabletMode = document.documentElement.hasAttribute("tabletmode"); + this[inTabletMode ? "_tabletCount" : "_desktopCount"]++; + }, + + finish() { + if (this.enabled) { + let histogram = Services.telemetry.getKeyedHistogramById( + "FX_TABLETMODE_PAGE_LOAD" + ); + histogram.add("tablet", this._tabletCount); + histogram.add("desktop", this._desktopCount); + } + }, +}; + +function displaySecurityInfo() { + BrowserPageInfo(null, "securityTab"); +} + +// Updates the UI density (for touch and compact mode) based on the uidensity pref. +var gUIDensity = { + MODE_NORMAL: 0, + MODE_COMPACT: 1, + MODE_TOUCH: 2, + uiDensityPref: "browser.uidensity", + autoTouchModePref: "browser.touchmode.auto", + + init() { + this.update(); + Services.prefs.addObserver(this.uiDensityPref, this); + Services.prefs.addObserver(this.autoTouchModePref, this); + }, + + uninit() { + Services.prefs.removeObserver(this.uiDensityPref, this); + Services.prefs.removeObserver(this.autoTouchModePref, this); + }, + + observe(aSubject, aTopic, aPrefName) { + if ( + aTopic != "nsPref:changed" || + (aPrefName != this.uiDensityPref && aPrefName != this.autoTouchModePref) + ) { + return; + } + + this.update(); + }, + + getCurrentDensity() { + // Automatically override the uidensity to touch in Windows tablet mode. + if ( + AppConstants.platform == "win" && + WindowsUIUtils.inTabletMode && + Services.prefs.getBoolPref(this.autoTouchModePref) + ) { + return { mode: this.MODE_TOUCH, overridden: true }; + } + return { + mode: Services.prefs.getIntPref(this.uiDensityPref), + overridden: false, + }; + }, + + update(mode) { + if (mode == null) { + mode = this.getCurrentDensity().mode; + } + + let docs = [document.documentElement]; + let shouldUpdateSidebar = SidebarUI.initialized && SidebarUI.isOpen; + if (shouldUpdateSidebar) { + docs.push(SidebarUI.browser.contentDocument.documentElement); + } + for (let doc of docs) { + switch (mode) { + case this.MODE_COMPACT: + doc.setAttribute("uidensity", "compact"); + break; + case this.MODE_TOUCH: + doc.setAttribute("uidensity", "touch"); + break; + default: + doc.removeAttribute("uidensity"); + break; + } + } + if (shouldUpdateSidebar) { + let tree = SidebarUI.browser.contentDocument.querySelector( + ".sidebar-placesTree" + ); + if (tree) { + // Tree items don't update their styles without changing some property on the + // parent tree element, like background-color or border. See bug 1407399. + tree.style.border = "1px"; + tree.style.border = ""; + } + } + + gBrowser.tabContainer.uiDensityChanged(); + gURLBar.updateLayoutBreakout(); + }, +}; + +const nodeToTooltipMap = { + "bookmarks-menu-button": "bookmarksMenuButton.tooltip", + "context-reload": "reloadButton.tooltip", + "context-stop": "stopButton.tooltip", + "downloads-button": "downloads.tooltip", + "fullscreen-button": "fullscreenButton.tooltip", + "appMenu-fullscreen-button2": "fullscreenButton.tooltip", + "new-window-button": "newWindowButton.tooltip", + "new-tab-button": "newTabButton.tooltip", + "tabs-newtab-button": "newTabButton.tooltip", + "reload-button": "reloadButton.tooltip", + "stop-button": "stopButton.tooltip", + "urlbar-zoom-button": "urlbar-zoom-button.tooltip", + "appMenu-zoomEnlarge-button2": "zoomEnlarge-button.tooltip", + "appMenu-zoomReset-button2": "zoomReset-button.tooltip", + "appMenu-zoomReduce-button2": "zoomReduce-button.tooltip", + "reader-mode-button": "reader-mode-button.tooltip", + "reader-mode-button-icon": "reader-mode-button.tooltip", +}; +const nodeToShortcutMap = { + "bookmarks-menu-button": "manBookmarkKb", + "context-reload": "key_reload", + "context-stop": "key_stop", + "downloads-button": "key_openDownloads", + "fullscreen-button": "key_enterFullScreen", + "appMenu-fullscreen-button2": "key_enterFullScreen", + "new-window-button": "key_newNavigator", + "new-tab-button": "key_newNavigatorTab", + "tabs-newtab-button": "key_newNavigatorTab", + "reload-button": "key_reload", + "stop-button": "key_stop", + "urlbar-zoom-button": "key_fullZoomReset", + "appMenu-zoomEnlarge-button2": "key_fullZoomEnlarge", + "appMenu-zoomReset-button2": "key_fullZoomReset", + "appMenu-zoomReduce-button2": "key_fullZoomReduce", + "reader-mode-button": "key_toggleReaderMode", + "reader-mode-button-icon": "key_toggleReaderMode", +}; + +const gDynamicTooltipCache = new Map(); +function GetDynamicShortcutTooltipText(nodeId) { + if (!gDynamicTooltipCache.has(nodeId) && nodeId in nodeToTooltipMap) { + let strId = nodeToTooltipMap[nodeId]; + let args = []; + if (nodeId in nodeToShortcutMap) { + let shortcutId = nodeToShortcutMap[nodeId]; + let shortcut = document.getElementById(shortcutId); + if (shortcut) { + args.push(ShortcutUtils.prettifyShortcut(shortcut)); + } + } + gDynamicTooltipCache.set( + nodeId, + gNavigatorBundle.getFormattedString(strId, args) + ); + } + return gDynamicTooltipCache.get(nodeId); +} + +function UpdateDynamicShortcutTooltipText(aTooltip) { + let nodeId = + aTooltip.triggerNode.id || aTooltip.triggerNode.getAttribute("anonid"); + aTooltip.setAttribute("label", GetDynamicShortcutTooltipText(nodeId)); +} + +/* + * - [ Dependencies ] --------------------------------------------------------- + * utilityOverlay.js: + * - gatherTextUnder + */ + +/** + * Extracts linkNode and href for the current click target. + * + * @param event + * The click event. + * @return [href, linkNode]. + * + * @note linkNode will be null if the click wasn't on an anchor + * element (or XLink). + */ +function hrefAndLinkNodeForClickEvent(event) { + function isHTMLLink(aNode) { + // Be consistent with what nsContextMenu.js does. + return ( + (HTMLAnchorElement.isInstance(aNode) && aNode.href) || + (HTMLAreaElement.isInstance(aNode) && aNode.href) || + HTMLLinkElement.isInstance(aNode) + ); + } + + let node = event.composedTarget; + while (node && !isHTMLLink(node)) { + node = node.flattenedTreeParentNode; + } + + if (node) { + return [node.href, node]; + } + + // If there is no linkNode, try simple XLink. + let href, baseURI; + node = event.composedTarget; + while (node && !href) { + if ( + node.nodeType == Node.ELEMENT_NODE && + (node.localName == "a" || + node.namespaceURI == "http://www.w3.org/1998/Math/MathML") + ) { + href = + node.getAttribute("href") || + node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + + if (href) { + baseURI = node.baseURI; + break; + } + } + node = node.flattenedTreeParentNode; + } + + // In case of XLink, we don't return the node we got href from since + // callers expect <a>-like elements. + return [href ? makeURLAbsolute(baseURI, href) : null, null]; +} + +/** + * Called whenever the user clicks in the content area. + * + * @param event + * The click event. + * @param isPanelClick + * Whether the event comes from an extension panel. + * @note default event is prevented if the click is handled. + */ +function contentAreaClick(event, isPanelClick) { + if (!event.isTrusted || event.defaultPrevented || event.button != 0) { + return; + } + + let [href, linkNode] = hrefAndLinkNodeForClickEvent(event); + if (!href) { + // Not a link, handle middle mouse navigation. + if ( + event.button == 1 && + Services.prefs.getBoolPref("middlemouse.contentLoadURL") && + !Services.prefs.getBoolPref("general.autoScroll") + ) { + middleMousePaste(event); + event.preventDefault(); + } + return; + } + + // This code only applies if we have a linkNode (i.e. clicks on real anchor + // elements, as opposed to XLink). + if ( + linkNode && + event.button == 0 && + !event.ctrlKey && + !event.shiftKey && + !event.altKey && + !event.metaKey + ) { + // An extension panel's links should target the main content area. Do this + // if no modifier keys are down and if there's no target or the target + // equals _main (the IE convention) or _content (the Mozilla convention). + let target = linkNode.target; + let mainTarget = !target || target == "_content" || target == "_main"; + if (isPanelClick && mainTarget) { + // javascript and data links should be executed in the current browser. + if ( + linkNode.getAttribute("onclick") || + href.startsWith("javascript:") || + href.startsWith("data:") + ) { + return; + } + + try { + urlSecurityCheck(href, linkNode.ownerDocument.nodePrincipal); + } catch (ex) { + // Prevent loading unsecure destinations. + event.preventDefault(); + return; + } + + openLinkIn(href, "current", { + allowThirdPartyFixup: false, + }); + event.preventDefault(); + return; + } + } + + handleLinkClick(event, href, linkNode); + + // Mark the page as a user followed link. This is done so that history can + // distinguish automatic embed visits from user activated ones. For example + // pages loaded in frames are embed visits and lost with the session, while + // visits across frames should be preserved. + try { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + PlacesUIUtils.markPageAsFollowedLink(href); + } + } catch (ex) { + /* Skip invalid URIs. */ + } +} + +/** + * Handles clicks on links. + * + * @return true if the click event was handled, false otherwise. + */ +function handleLinkClick(event, href, linkNode) { + if (event.button == 2) { + // right click + return false; + } + + var where = whereToOpenLink(event); + if (where == "current") { + return false; + } + + var doc = event.target.ownerDocument; + let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance( + Ci.nsIReferrerInfo + ); + if (linkNode) { + referrerInfo.initWithElement(linkNode); + } else { + referrerInfo.initWithDocument(doc); + } + + if (where == "save") { + saveURL( + href, + null, + linkNode ? gatherTextUnder(linkNode) : "", + null, + true, + true, + referrerInfo, + doc.cookieJarSettings, + doc + ); + event.preventDefault(); + return true; + } + + let frameID = WebNavigationFrames.getFrameId(doc.defaultView); + + urlSecurityCheck(href, doc.nodePrincipal); + let params = { + charset: doc.characterSet, + referrerInfo, + originPrincipal: doc.nodePrincipal, + originStoragePrincipal: doc.effectiveStoragePrincipal, + triggeringPrincipal: doc.nodePrincipal, + csp: doc.csp, + frameID, + }; + + // The new tab/window must use the same userContextId + if (doc.nodePrincipal.originAttributes.userContextId) { + params.userContextId = doc.nodePrincipal.originAttributes.userContextId; + } + + openLinkIn(href, where, params); + event.preventDefault(); + return true; +} + +/** + * Handles paste on middle mouse clicks. + * + * @param event {Event | Object} Event or JSON object. + */ +function middleMousePaste(event) { + let clipboard = readFromClipboard(); + if (!clipboard) { + return; + } + + // Strip embedded newlines and surrounding whitespace, to match the URL + // bar's behavior (stripsurroundingwhitespace) + clipboard = clipboard.replace(/\s*\n\s*/g, ""); + + clipboard = UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard); + + // if it's not the current tab, we don't need to do anything because the + // browser doesn't exist. + let where = whereToOpenLink(event, true, false); + let lastLocationChange; + if (where == "current") { + lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + } + + UrlbarUtils.getShortcutOrURIAndPostData(clipboard).then(data => { + try { + makeURI(data.url); + } catch (ex) { + // Not a valid URI. + return; + } + + try { + UrlbarUtils.addToUrlbarHistory(data.url, window); + } catch (ex) { + // Things may go wrong when adding url to session history, + // but don't let that interfere with the loading of the url. + console.error(ex); + } + + if ( + where != "current" || + lastLocationChange == gBrowser.selectedBrowser.lastLocationChange + ) { + openUILink(data.url, event, { + ignoreButton: true, + allowInheritPrincipal: data.mayInheritPrincipal, + triggeringPrincipal: gBrowser.selectedBrowser.contentPrincipal, + csp: gBrowser.selectedBrowser.csp, + }); + } + }); + + if (Event.isInstance(event)) { + event.stopPropagation(); + } +} + +// handleDroppedLink has the following 2 overloads: +// handleDroppedLink(event, url, name, triggeringPrincipal) +// handleDroppedLink(event, links, triggeringPrincipal) +function handleDroppedLink( + event, + urlOrLinks, + nameOrTriggeringPrincipal, + triggeringPrincipal +) { + let links; + if (Array.isArray(urlOrLinks)) { + links = urlOrLinks; + triggeringPrincipal = nameOrTriggeringPrincipal; + } else { + links = [{ url: urlOrLinks, nameOrTriggeringPrincipal, type: "" }]; + } + + let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + + let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); + + // event is null if links are dropped in content process. + // inBackground should be false, as it's loading into current browser. + let inBackground = false; + if (event) { + inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (event.shiftKey) { + inBackground = !inBackground; + } + } + + (async function () { + if ( + links.length >= + Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn") + ) { + // Sync dialog cannot be used inside drop event handler. + let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs( + links.length, + window + ); + if (!answer) { + return; + } + } + + let urls = []; + let postDatas = []; + for (let link of links) { + let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url); + urls.push(data.url); + postDatas.push(data.postData); + } + if (lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) { + gBrowser.loadTabs(urls, { + inBackground, + replace: true, + allowThirdPartyFixup: false, + postDatas, + userContextId, + triggeringPrincipal, + }); + } + })(); + + // If links are dropped in content process, event.preventDefault() should be + // called in content process. + if (event) { + // Keep the event from being handled by the dragDrop listeners + // built-in to gecko if they happen to be above us. + event.preventDefault(); + } +} + +function BrowserForceEncodingDetection() { + gBrowser.selectedBrowser.forceEncodingDetection(); + BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); +} + +var ToolbarContextMenu = { + updateDownloadsAutoHide(popup) { + let checkbox = document.getElementById( + "toolbar-context-autohide-downloads-button" + ); + let isDownloads = + popup.triggerNode && + ["downloads-button", "wrapper-downloads-button"].includes( + popup.triggerNode.id + ); + checkbox.hidden = !isDownloads; + if (DownloadsButton.autoHideDownloadsButton) { + checkbox.setAttribute("checked", "true"); + } else { + checkbox.removeAttribute("checked"); + } + }, + + onDownloadsAutoHideChange(event) { + let autoHide = event.target.getAttribute("checked") == "true"; + Services.prefs.setBoolPref("browser.download.autohideButton", autoHide); + }, + + updateDownloadsAlwaysOpenPanel(popup) { + let separator = document.getElementById( + "toolbarDownloadsAnchorMenuSeparator" + ); + let checkbox = document.getElementById( + "toolbar-context-always-open-downloads-panel" + ); + let isDownloads = + popup.triggerNode && + ["downloads-button", "wrapper-downloads-button"].includes( + popup.triggerNode.id + ); + separator.hidden = checkbox.hidden = !isDownloads; + gAlwaysOpenPanel + ? checkbox.setAttribute("checked", "true") + : checkbox.removeAttribute("checked"); + }, + + onDownloadsAlwaysOpenPanelChange(event) { + let alwaysOpen = event.target.getAttribute("checked") == "true"; + Services.prefs.setBoolPref("browser.download.alwaysOpenPanel", alwaysOpen); + }, + + _getUnwrappedTriggerNode(popup) { + // Toolbar buttons are wrapped in customize mode. Unwrap if necessary. + let { triggerNode } = popup; + if (triggerNode && gCustomizeMode.isWrappedToolbarItem(triggerNode)) { + return triggerNode.firstElementChild; + } + return triggerNode; + }, + + _getExtensionId(popup) { + let node = this._getUnwrappedTriggerNode(popup); + return node && node.getAttribute("data-extensionid"); + }, + + _getWidgetId(popup) { + let node = this._getUnwrappedTriggerNode(popup); + return node?.closest(".unified-extensions-item")?.id; + }, + + async updateExtension(popup, event) { + let removeExtension = popup.querySelector( + ".customize-context-removeExtension" + ); + let manageExtension = popup.querySelector( + ".customize-context-manageExtension" + ); + let reportExtension = popup.querySelector( + ".customize-context-reportExtension" + ); + let pinToToolbar = popup.querySelector(".customize-context-pinToToolbar"); + let separator = reportExtension.nextElementSibling; + let id = this._getExtensionId(popup); + let addon = id && (await AddonManager.getAddonByID(id)); + + for (let element of [removeExtension, manageExtension, separator]) { + element.hidden = !addon; + } + + if (pinToToolbar) { + pinToToolbar.hidden = !addon; + } + + reportExtension.hidden = !addon || !gAddonAbuseReportEnabled; + + if (addon) { + popup.querySelector(".customize-context-moveToPanel").hidden = true; + popup.querySelector(".customize-context-removeFromToolbar").hidden = true; + + if (pinToToolbar) { + let widgetId = this._getWidgetId(popup); + if (widgetId) { + let area = CustomizableUI.getPlacementOfWidget(widgetId).area; + let inToolbar = area != CustomizableUI.AREA_ADDONS; + pinToToolbar.setAttribute("checked", inToolbar); + } + } + + removeExtension.disabled = !( + addon.permissions & AddonManager.PERM_CAN_UNINSTALL + ); + + if (event?.target?.id === "toolbar-context-menu") { + ExtensionsUI.originControlsMenu(popup, id); + } + } + }, + + async removeExtensionForContextAction(popup) { + let id = this._getExtensionId(popup); + await BrowserAddonUI.removeAddon(id, "browserAction"); + }, + + async reportExtensionForContextAction(popup, reportEntryPoint) { + let id = this._getExtensionId(popup); + await BrowserAddonUI.reportAddon(id, reportEntryPoint); + }, + + async openAboutAddonsForContextAction(popup) { + let id = this._getExtensionId(popup); + await BrowserAddonUI.manageAddon(id, "browserAction"); + }, +}; + +// Note that this is also called from non-browser windows on OSX, which do +// share menu items but not much else. See nonbrowser-mac.js. +var BrowserOffline = { + _inited: false, + + // BrowserOffline Public Methods + init() { + if (!this._uiElement) { + this._uiElement = document.getElementById("cmd_toggleOfflineStatus"); + } + + Services.obs.addObserver(this, "network:offline-status-changed"); + + this._updateOfflineUI(Services.io.offline); + + this._inited = true; + }, + + uninit() { + if (this._inited) { + Services.obs.removeObserver(this, "network:offline-status-changed"); + } + }, + + toggleOfflineStatus() { + var ioService = Services.io; + + if (!ioService.offline && !this._canGoOffline()) { + this._updateOfflineUI(false); + return; + } + + ioService.offline = !ioService.offline; + }, + + // nsIObserver + observe(aSubject, aTopic, aState) { + if (aTopic != "network:offline-status-changed") { + return; + } + + // This notification is also received because of a loss in connectivity, + // which we ignore by updating the UI to the current value of io.offline + this._updateOfflineUI(Services.io.offline); + }, + + // BrowserOffline Implementation Methods + _canGoOffline() { + try { + var cancelGoOffline = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers(cancelGoOffline, "offline-requested"); + + // Something aborted the quit process. + if (cancelGoOffline.data) { + return false; + } + } catch (ex) {} + + return true; + }, + + _uiElement: null, + _updateOfflineUI(aOffline) { + var offlineLocked = Services.prefs.prefIsLocked("network.online"); + if (offlineLocked) { + this._uiElement.setAttribute("disabled", "true"); + } + + this._uiElement.setAttribute("checked", aOffline); + }, +}; + +var CanvasPermissionPromptHelper = { + _permissionsPrompt: "canvas-permissions-prompt", + _permissionsPromptHideDoorHanger: "canvas-permissions-prompt-hide-doorhanger", + _notificationIcon: "canvas-notification-icon", + + init() { + Services.obs.addObserver(this, this._permissionsPrompt); + Services.obs.addObserver(this, this._permissionsPromptHideDoorHanger); + }, + + uninit() { + Services.obs.removeObserver(this, this._permissionsPrompt); + Services.obs.removeObserver(this, this._permissionsPromptHideDoorHanger); + }, + + // aSubject is an nsIBrowser (e10s) or an nsIDOMWindow (non-e10s). + // aData is an Origin string. + observe(aSubject, aTopic, aData) { + if ( + aTopic != this._permissionsPrompt && + aTopic != this._permissionsPromptHideDoorHanger + ) { + return; + } + + let browser; + if (aSubject instanceof Ci.nsIDOMWindow) { + browser = aSubject.docShell.chromeEventHandler; + } else { + browser = aSubject; + } + + if (gBrowser.selectedBrowser !== browser) { + // Must belong to some other window. + return; + } + + let message = gNavigatorBundle.getFormattedString( + "canvas.siteprompt2", + ["<>"], + 1 + ); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(aData); + + function setCanvasPermission(aPerm, aPersistent) { + Services.perms.addFromPrincipal( + principal, + "canvas", + aPerm, + aPersistent + ? Ci.nsIPermissionManager.EXPIRE_NEVER + : Ci.nsIPermissionManager.EXPIRE_SESSION + ); + } + + let mainAction = { + label: gNavigatorBundle.getString("canvas.allow2"), + accessKey: gNavigatorBundle.getString("canvas.allow2.accesskey"), + callback(state) { + setCanvasPermission( + Ci.nsIPermissionManager.ALLOW_ACTION, + state && state.checkboxChecked + ); + }, + }; + + let secondaryActions = [ + { + label: gNavigatorBundle.getString("canvas.block"), + accessKey: gNavigatorBundle.getString("canvas.block.accesskey"), + callback(state) { + setCanvasPermission( + Ci.nsIPermissionManager.DENY_ACTION, + state && state.checkboxChecked + ); + }, + }, + ]; + + let checkbox = { + // In PB mode, we don't want the "always remember" checkbox + show: !PrivateBrowsingUtils.isWindowPrivate(window), + }; + if (checkbox.show) { + checkbox.checked = true; + checkbox.label = gBrowserBundle.GetStringFromName("canvas.remember2"); + } + + let options = { + checkbox, + name: principal.host, + learnMoreURL: + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "fingerprint-permission", + dismissed: aTopic == this._permissionsPromptHideDoorHanger, + eventCallback(e) { + if (e == "showing") { + this.browser.ownerDocument.getElementById( + "canvas-permissions-prompt-warning" + ).textContent = gBrowserBundle.GetStringFromName( + "canvas.siteprompt2.warning" + ); + } + }, + }; + PopupNotifications.show( + browser, + this._permissionsPrompt, + message, + this._notificationIcon, + mainAction, + secondaryActions, + options + ); + }, +}; + +var WebAuthnPromptHelper = { + _icon: "webauthn-notification-icon", + _topic: "webauthn-prompt", + + // The current notification, if any. The U2F manager is a singleton, we will + // never allow more than one active request. And thus we'll never have more + // than one notification either. + _current: null, + + // The current transaction ID. Will be checked when we're notified of the + // cancellation of an ongoing WebAuthhn request. + _tid: 0, + + // Translation object + _l10n: null, + + init() { + this._l10n = new Localization(["browser/webauthnDialog.ftl"], true); + Services.obs.addObserver(this, this._topic); + }, + + uninit() { + Services.obs.removeObserver(this, this._topic); + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "fullscreen-nav-toolbox": + // Prevent the navigation toolbox from being hidden while a WebAuthn + // prompt is visible. + if (aData == "hidden" && this._tid != 0) { + FullScreen.showNavToolbox(); + } + return; + case "fullscreen-painted": + // Prevent DOM elements from going fullscreen while a WebAuthn + // prompt is shown. + if (this._tid != 0) { + FullScreen.exitDomFullScreen(); + } + return; + case this._topic: + break; + default: + return; + } + // aTopic is equal to this._topic + + let data = JSON.parse(aData); + + // If we receive a cancel, it might be a WebAuthn prompt starting in another + // window, and the other window's browsing context will send out the + // cancellations, so any cancel action we get should prompt us to cancel. + if (data.prompt.type == "cancel") { + this.cancel(data); + return; + } + + if ( + data.browsingContextId !== gBrowser.selectedBrowser.browsingContext.id + ) { + // Must belong to some other window. + return; + } + + let mgr = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + if (data.prompt.type == "presence") { + this.presence_required(mgr, data); + } else if (data.prompt.type == "register-direct") { + this.registerDirect(mgr, data); + } else if (data.prompt.type == "pin-required") { + this.pin_required(mgr, false, data); + } else if (data.prompt.type == "pin-invalid") { + this.pin_required(mgr, true, data); + } else if (data.prompt.type == "select-sign-result") { + this.select_sign_result(mgr, data); + } else if (data.prompt.type == "already-registered") { + this.show_info( + mgr, + data.origin, + data.tid, + "alreadyRegistered", + "webauthn.alreadyRegisteredPrompt" + ); + } else if (data.prompt.type == "select-device") { + this.show_info( + mgr, + data.origin, + data.tid, + "selectDevice", + "webauthn.selectDevicePrompt" + ); + } else if (data.prompt.type == "pin-auth-blocked") { + this.show_info( + mgr, + data.origin, + data.tid, + "pinAuthBlocked", + "webauthn.pinAuthBlockedPrompt" + ); + } else if (data.prompt.type == "uv-blocked") { + this.show_info( + mgr, + data.origin, + data.tid, + "uvBlocked", + "webauthn.uvBlockedPrompt" + ); + } else if (data.prompt.type == "uv-invalid") { + let retriesLeft = data.prompt.retries; + let dialogText; + if (retriesLeft == 0) { + // We can skip that because it will either be replaced + // by uv-blocked or by PIN-prompt + return; + } else if (retriesLeft < 0) { + dialogText = this._l10n.formatValueSync( + "webauthn-uv-invalid-short-prompt" + ); + } else { + dialogText = this._l10n.formatValueSync( + "webauthn-uv-invalid-long-prompt", + { retriesLeft } + ); + } + let mainAction = this.buildCancelAction(mgr, data.tid); + this.show_formatted_msg(data.tid, "uvInvalid", dialogText, mainAction); + } else if (data.prompt.type == "device-blocked") { + this.show_info( + mgr, + data.origin, + data.tid, + "deviceBlocked", + "webauthn.deviceBlockedPrompt" + ); + } else if (data.prompt.type == "pin-not-set") { + this.show_info( + mgr, + data.origin, + data.tid, + "pinNotSet", + "webauthn.pinNotSetPrompt" + ); + } + }, + + prompt_for_password(origin, wasInvalid, retriesLeft, aPassword) { + this.reset(); + let dialogText; + if (!wasInvalid) { + dialogText = this._l10n.formatValueSync("webauthn-pin-required-prompt"); + } else if (retriesLeft < 0 || retriesLeft > 3) { + // The token will need to be power cycled after three incorrect attempts, + // so we show a short error message that does not include retriesLeft. It + // would be confusing to display retriesLeft at this point, as the user + // will feel that they only get three attempts. + dialogText = this._l10n.formatValueSync( + "webauthn-pin-invalid-short-prompt" + ); + } else { + // The user is close to having their PIN permanently blocked. Show a more + // severe warning that includes the retriesLeft counter. + dialogText = this._l10n.formatValueSync( + "webauthn-pin-invalid-long-prompt", + { retriesLeft } + ); + } + + let res = Services.prompt.promptPasswordBC( + gBrowser.selectedBrowser.browsingContext, + Services.prompt.MODAL_TYPE_TAB, + origin, + dialogText, + aPassword + ); + return res; + }, + + select_sign_result(mgr, { origin, tid, prompt: { entities } }) { + let unknownAccount = this._l10n.formatValueSync( + "webauthn-select-sign-result-unknown-account" + ); + let secondaryActions = []; + for (let i = 0; i < entities.length; i++) { + let label = entities[i].name ?? unknownAccount; + secondaryActions.push({ + label, + accessKey: i.toString(), + callback(aState) { + mgr.selectionCallback(tid, i); + }, + }); + } + let mainAction = this.buildCancelAction(mgr, tid); + let options = { escAction: "buttoncommand" }; + this.show( + tid, + "select-sign-result", + "webauthn.selectSignResultPrompt", + origin, + mainAction, + secondaryActions, + options + ); + }, + + pin_required(mgr, wasInvalid, { origin, tid, prompt: { retries } }) { + let aPassword = Object.create(null); // create a "null" object + let res = this.prompt_for_password(origin, wasInvalid, retries, aPassword); + if (res) { + mgr.pinCallback(tid, aPassword.value); + } else { + mgr.cancel(tid); + } + }, + + presence_required(mgr, { origin, tid }) { + let mainAction = this.buildCancelAction(mgr, tid); + let options = { escAction: "buttoncommand" }; + let secondaryActions = []; + let message = "webauthn.userPresencePrompt"; + this.show( + tid, + "presence", + message, + origin, + mainAction, + secondaryActions, + options + ); + }, + + registerDirect(mgr, { origin, tid }) { + let mainAction = this.buildProceedAction(mgr, tid); + let secondaryActions = [this.buildCancelAction(mgr, tid)]; + + let learnMoreURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "webauthn-direct-attestation"; + + let options = { + learnMoreURL, + checkbox: { + label: gNavigatorBundle.getString("webauthn.anonymize"), + }, + hintText: "webauthn.registerDirectPromptHint", + }; + this.show( + tid, + "register-direct", + "webauthn.registerDirectPrompt3", + origin, + mainAction, + secondaryActions, + options + ); + }, + + show_info(mgr, origin, tid, id, stringId) { + let mainAction = this.buildCancelAction(mgr, tid); + this.show(tid, id, stringId, origin, mainAction); + }, + + show( + tid, + id, + stringId, + origin, + mainAction, + secondaryActions = [], + options = {} + ) { + let brandShortName = document + .getElementById("bundle_brand") + .getString("brandShortName"); + let message = gNavigatorBundle.getFormattedString(stringId, [ + "<>", + brandShortName, + ]); + + try { + origin = Services.io.newURI(origin).asciiHost; + } catch (e) { + /* Might fail for arbitrary U2F RP IDs. */ + } + options.name = origin; + this.show_formatted_msg( + tid, + id, + message, + mainAction, + secondaryActions, + options + ); + }, + + show_formatted_msg( + tid, + id, + message, + mainAction, + secondaryActions = [], + options = {} + ) { + this.reset(); + this._tid = tid; + + // We need to prevent some fullscreen transitions while WebAuthn prompts + // are shown. The `fullscreen-painted` topic is notified when DOM elements + // go fullscreen. + Services.obs.addObserver(this, "fullscreen-painted"); + + // The `fullscreen-nav-toolbox` topic is notified when the nav toolbox is + // hidden. + Services.obs.addObserver(this, "fullscreen-nav-toolbox"); + + // Ensure that no DOM elements are already fullscreen. + FullScreen.exitDomFullScreen(); + + // Ensure that the nav toolbox is being shown. + if (window.fullScreen) { + FullScreen.showNavToolbox(); + } + + let brandShortName = document + .getElementById("bundle_brand") + .getString("brandShortName"); + if (options.hintText) { + options.hintText = gNavigatorBundle.getFormattedString(options.hintText, [ + brandShortName, + ]); + } + + options.hideClose = true; + options.persistent = true; + options.eventCallback = event => { + if (event == "removed") { + Services.obs.removeObserver(this, "fullscreen-painted"); + Services.obs.removeObserver(this, "fullscreen-nav-toolbox"); + this._current = null; + this._tid = 0; + } + }; + + this._current = PopupNotifications.show( + gBrowser.selectedBrowser, + `webauthn-prompt-${id}`, + message, + this._icon, + mainAction, + secondaryActions, + options + ); + }, + + cancel({ tid }) { + if (this._tid == tid) { + this.reset(); + } + }, + + reset() { + if (this._current) { + this._current.remove(); + } + }, + + buildProceedAction(mgr, tid) { + return { + label: gNavigatorBundle.getString("webauthn.proceed"), + accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"), + callback(state) { + mgr.resumeMakeCredential(tid, state.checkboxChecked); + }, + }; + }, + + buildCancelAction(mgr, tid) { + return { + label: gNavigatorBundle.getString("webauthn.cancel"), + accessKey: gNavigatorBundle.getString("webauthn.cancel.accesskey"), + callback() { + mgr.cancel(tid); + }, + }; + }, +}; + +function CanCloseWindow() { + // Avoid redundant calls to canClose from showing multiple + // PermitUnload dialogs. + if (Services.startup.shuttingDown || window.skipNextCanClose) { + return true; + } + + for (let browser of gBrowser.browsers) { + // Don't instantiate lazy browsers. + if (!browser.isConnected) { + continue; + } + + let { permitUnload } = browser.permitUnload(); + if (!permitUnload) { + return false; + } + } + return true; +} + +function WindowIsClosing(event) { + let source; + if (event) { + let target = event.sourceEvent?.target; + if (target?.id?.startsWith("menu_")) { + source = "menuitem"; + } else if (target?.nodeName == "toolbarbutton") { + source = "close-button"; + } else { + let key = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey"; + source = event[key] ? "shortcut" : "OS"; + } + } + if (!closeWindow(false, warnAboutClosingWindow, source)) { + return false; + } + + // In theory we should exit here and the Window's internal Close + // method should trigger canClose on nsBrowserAccess. However, by + // that point it's too late to be able to show a prompt for + // PermitUnload. So we do it here, when we still can. + if (CanCloseWindow()) { + // This flag ensures that the later canClose call does nothing. + // It's only needed to make tests pass, since they detect the + // prompt even when it's not actually shown. + window.skipNextCanClose = true; + return true; + } + + return false; +} + +/** + * Checks if this is the last full *browser* window around. If it is, this will + * be communicated like quitting. Otherwise, we warn about closing multiple tabs. + * + * @param source where the request to close came from (used for telemetry) + * @returns true if closing can proceed, false if it got cancelled. + */ +function warnAboutClosingWindow(source) { + // Popups aren't considered full browser windows; we also ignore private windows. + let isPBWindow = + PrivateBrowsingUtils.isWindowPrivate(window) && + !PrivateBrowsingUtils.permanentPrivateBrowsing; + + if (!isPBWindow && !toolbar.visible) { + return gBrowser.warnAboutClosingTabs( + gBrowser.visibleTabs.length, + gBrowser.closingTabsEnum.ALL, + source + ); + } + + // Figure out if there's at least one other browser window around. + let otherPBWindowExists = false; + let otherWindowExists = false; + for (let win of browserWindows()) { + if (!win.closed && win != window) { + otherWindowExists = true; + if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win)) { + otherPBWindowExists = true; + } + // If the current window is not in private browsing mode we don't need to + // look for other pb windows, we can leave the loop when finding the + // first non-popup window. If however the current window is in private + // browsing mode then we need at least one other pb and one non-popup + // window to break out early. + if (!isPBWindow || otherPBWindowExists) { + break; + } + } + } + + if (isPBWindow && !otherPBWindowExists) { + let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + exitingCanceled.data = false; + Services.obs.notifyObservers(exitingCanceled, "last-pb-context-exiting"); + if (exitingCanceled.data) { + return false; + } + } + + if (otherWindowExists) { + return ( + isPBWindow || + gBrowser.warnAboutClosingTabs( + gBrowser.visibleTabs.length, + gBrowser.closingTabsEnum.ALL, + source + ) + ); + } + + let os = Services.obs; + + let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + os.notifyObservers(closingCanceled, "browser-lastwindow-close-requested"); + if (closingCanceled.data) { + return false; + } + + os.notifyObservers(null, "browser-lastwindow-close-granted"); + + // OS X doesn't quit the application when the last window is closed, but keeps + // the session alive. Hence don't prompt users to save tabs, but warn about + // closing multiple tabs. + return ( + AppConstants.platform != "macosx" || + isPBWindow || + gBrowser.warnAboutClosingTabs( + gBrowser.visibleTabs.length, + gBrowser.closingTabsEnum.ALL, + source + ) + ); +} + +var MailIntegration = { + sendLinkForBrowser(aBrowser) { + this.sendMessage( + gURLBar.makeURIReadable(aBrowser.currentURI).displaySpec, + aBrowser.contentTitle + ); + }, + + sendMessage(aBody, aSubject) { + // generate a mailto url based on the url and the url's title + var mailtoUrl = "mailto:"; + if (aBody) { + mailtoUrl += "?body=" + encodeURIComponent(aBody); + mailtoUrl += "&subject=" + encodeURIComponent(aSubject); + } + + var uri = makeURI(mailtoUrl); + + // now pass this uri to the operating system + this._launchExternalUrl(uri); + }, + + // a generic method which can be used to pass arbitrary urls to the operating + // system. + // aURL --> a nsIURI which represents the url to launch + _launchExternalUrl(aURL) { + var extProtocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + if (extProtocolSvc) { + extProtocolSvc.loadURI( + aURL, + Services.scriptSecurityManager.getSystemPrincipal() + ); + } + }, +}; + +/** + * Open about:addons page by given view id. + * @param {String} aView + * View id of page that will open. + * e.g. "addons://discover/" + * @param {Object} options + * { + * selectTabByViewId: If true, if there is the tab opening page having + * same view id, select the tab. Else if the current + * page is blank, load on it. Otherwise, open a new + * tab, then load on it. + * If false, if there is the tab opening + * about:addoons page, select the tab and load page + * for view id on it. Otherwise, leave the loading + * behavior to switchToTabHavingURI(). + * If no options, handles as false. + * } + * @returns {Promise} When the Promise resolves, returns window object loaded the + * view id. + */ +function BrowserOpenAddonsMgr(aView, { selectTabByViewId = false } = {}) { + return new Promise(resolve => { + let emWindow; + let browserWindow; + + var receivePong = function (aSubject, aTopic, aData) { + let browserWin = aSubject.browsingContext.topChromeWindow; + if (!emWindow || browserWin == window /* favor the current window */) { + if ( + selectTabByViewId && + aSubject.gViewController.currentViewId !== aView + ) { + return; + } + + emWindow = aSubject; + browserWindow = browserWin; + } + }; + Services.obs.addObserver(receivePong, "EM-pong"); + Services.obs.notifyObservers(null, "EM-ping"); + Services.obs.removeObserver(receivePong, "EM-pong"); + + if (emWindow) { + if (aView && !selectTabByViewId) { + emWindow.loadView(aView); + } + let tab = browserWindow.gBrowser.getTabForBrowser( + emWindow.docShell.chromeEventHandler + ); + browserWindow.gBrowser.selectedTab = tab; + emWindow.focus(); + resolve(emWindow); + return; + } + + if (selectTabByViewId) { + const target = isBlankPageURL(gBrowser.currentURI.spec) + ? "current" + : "tab"; + openTrustedLinkIn("about:addons", target); + } else { + // This must be a new load, else the ping/pong would have + // found the window above. + switchToTabHavingURI("about:addons", true); + } + + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.removeObserver(observer, aTopic); + if (aView) { + aSubject.loadView(aView); + } + aSubject.focus(); + resolve(aSubject); + }, "EM-loaded"); + }); +} + +function AddKeywordForSearchField() { + if (!gContextMenu) { + throw new Error("Context menu doesn't seem to be open."); + } + + gContextMenu.addKeywordForSearchField(); +} + +/** + * Applies only to the cmd|ctrl + shift + T keyboard shortcut + * Undo the last action that was taken - either closing the last tab or closing the last window; + * If none of those were the last actions, restore the last session if possible. + */ +function restoreLastClosedTabOrWindowOrSession() { + let lastActionTaken = SessionStore.popLastClosedAction(); + + if (lastActionTaken) { + switch (lastActionTaken.type) { + case SessionStore.LAST_ACTION_CLOSED_TAB: { + undoCloseTab(); + break; + } + case SessionStore.LAST_ACTION_CLOSED_WINDOW: { + undoCloseWindow(); + break; + } + } + } else { + let closedTabCount = SessionStore.getLastClosedTabCount(window); + if (SessionStore.canRestoreLastSession) { + SessionStore.restoreLastSession(); + } else if (closedTabCount) { + // we need to support users who have automatic session restore enabled + undoCloseTab(); + } + } +} + +/** + * Re-open a closed tab into the current window. + * @param [aIndex] + * The index of the tab (via SessionStore.getClosedTabData). + * When undefined, the first n closed tabs will be re-opened, where n is provided by getLastClosedTabCount. + * @param {string} [sourceWindowSSId] + * An optional sessionstore id to identify the source window for the tab. + * I.e. the window the tab belonged to when closed. + * When undefined we'll use the current window + * @returns a reference to the reopened tab. + */ +function undoCloseTab(aIndex, sourceWindowSSId) { + // the window we'll open the tab into + let targetWindow = window; + // the window the tab was closed from + let sourceWindow; + if (sourceWindowSSId) { + sourceWindow = SessionStore.getWindowById(sourceWindowSSId); + if (!sourceWindow) { + throw new Error( + "sourceWindowSSId argument to undoCloseTab didn't resolve to a window" + ); + } + } else { + sourceWindow = window; + } + + // wallpaper patch to prevent an unnecessary blank tab (bug 343895) + let blankTabToRemove = null; + if ( + targetWindow.gBrowser.visibleTabs.length == 1 && + targetWindow.gBrowser.selectedTab.isEmpty + ) { + blankTabToRemove = targetWindow.gBrowser.selectedTab; + } + + // We are specifically interested in the lastClosedTabCount for the source window. + // When aIndex is undefined, we restore all the lastClosedTabCount tabs. + let lastClosedTabCount = SessionStore.getLastClosedTabCount(sourceWindow); + let tab = null; + // aIndex is undefined if the function is called without a specific tab to restore. + let tabsToRemove = + aIndex !== undefined ? [aIndex] : new Array(lastClosedTabCount).fill(0); + let tabsRemoved = false; + for (let index of tabsToRemove) { + if (SessionStore.getClosedTabCountForWindow(sourceWindow) > index) { + tab = SessionStore.undoCloseTab(sourceWindow, index, targetWindow); + tabsRemoved = true; + } + } + + if (tabsRemoved && blankTabToRemove) { + targetWindow.gBrowser.removeTab(blankTabToRemove); + } + + return tab; +} + +/** + * Re-open a closed window. + * @param aIndex + * The index of the window (via SessionStore.getClosedWindowData) + * @returns a reference to the reopened window. + */ +function undoCloseWindow(aIndex) { + let window = null; + if (SessionStore.getClosedWindowCount() > (aIndex || 0)) { + window = SessionStore.undoCloseWindow(aIndex || 0); + } + + return window; +} + +function ReportFalseDeceptiveSite() { + let contextsToVisit = [gBrowser.selectedBrowser.browsingContext]; + while (contextsToVisit.length) { + let currentContext = contextsToVisit.pop(); + let global = currentContext.currentWindowGlobal; + + if (!global) { + continue; + } + let docURI = global.documentURI; + // Ensure the page is an about:blocked pagae before handling. + if (docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked")) { + let actor = global.getActor("BlockedSite"); + actor.sendQuery("DeceptiveBlockedDetails").then(data => { + let reportUrl = gSafeBrowsing.getReportURL( + "PhishMistake", + data.blockedInfo + ); + if (reportUrl) { + openTrustedLinkIn(reportUrl, "tab"); + } else { + let bundle = Services.strings.createBundle( + "chrome://browser/locale/safebrowsing/safebrowsing.properties" + ); + Services.prompt.alert( + window, + bundle.GetStringFromName("errorReportFalseDeceptiveTitle"), + bundle.formatStringFromName("errorReportFalseDeceptiveMessage", [ + data.blockedInfo.provider, + ]) + ); + } + }); + } + + contextsToVisit.push(...currentContext.children); + } +} + +/** + * This is a temporary hack to connect a Help menu item for reporting + * site issues to the WebCompat team's Site Compatability Reporter + * WebExtension, which ships by default and is enabled on pre-release + * channels. + * + * Once we determine if Help is the right place for it, we'll do something + * slightly better than this. + * + * See bug 1690573. + */ +function ReportSiteIssue() { + let subject = { wrappedJSObject: gBrowser.selectedTab }; + Services.obs.notifyObservers(subject, "report-site-issue"); +} + +/** + * When the browser is being controlled from out-of-process, + * e.g. when Marionette or the remote debugging protocol is used, + * we add a visual hint to the browser UI to indicate to the user + * that the browser session is under remote control. + * + * This is called when the content browser initialises (from gBrowserInit.onLoad()) + * and when the "remote-listening" system notification fires. + */ +const gRemoteControl = { + observe(subject, topic, data) { + gRemoteControl.updateVisualCue(); + }, + + updateVisualCue() { + // Disable updating the remote control cue for performance tests, + // because these could fail due to an early initialization of Marionette. + const disableRemoteControlCue = Services.prefs.getBoolPref( + "browser.chrome.disableRemoteControlCueForTests", + false + ); + if (disableRemoteControlCue && Cu.isInAutomation) { + return; + } + + const mainWindow = document.documentElement; + const remoteControlComponent = this.getRemoteControlComponent(); + if (remoteControlComponent) { + mainWindow.setAttribute("remotecontrol", "true"); + const remoteControlIcon = document.getElementById("remote-control-icon"); + document.l10n.setAttributes( + remoteControlIcon, + "urlbar-remote-control-notification-anchor2", + { component: remoteControlComponent } + ); + } else { + mainWindow.removeAttribute("remotecontrol"); + } + }, + + getRemoteControlComponent() { + // For DevTools sockets, only show the remote control cue if the socket is + // not coming from a regular Browser Toolbox debugging session. + if ( + DevToolsSocketStatus.hasSocketOpened({ + excludeBrowserToolboxSockets: true, + }) + ) { + return "DevTools"; + } + + if (Marionette.running) { + return "Marionette"; + } + + if (RemoteAgent.running) { + return "RemoteAgent"; + } + + return null; + }, +}; + +// Note that this is also called from non-browser windows on OSX, which do +// share menu items but not much else. See nonbrowser-mac.js. +var gPrivateBrowsingUI = { + init: function PBUI_init() { + // Do nothing for normal windows + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + + // Disable the Clear Recent History... menu item when in PB mode + // temporary fix until bug 463607 is fixed + document.getElementById("Tools:Sanitize").setAttribute("disabled", "true"); + + if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + return; + } + + // Adjust the window's title + let docElement = document.documentElement; + docElement.setAttribute( + "privatebrowsingmode", + PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary" + ); + + gBrowser.updateTitlebar(); + + // Bug 1846583 - hide pocket button in PBM + if (gUseFeltPrivacyUI) { + const saveToPocketButton = document.getElementById( + "save-to-pocket-button" + ); + if (saveToPocketButton) { + saveToPocketButton.remove(); + document.documentElement.setAttribute("pocketdisabled", "true"); + } + } + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Adjust the New Window menu entries + let newWindow = document.getElementById("menu_newNavigator"); + let newPrivateWindow = document.getElementById("menu_newPrivateWindow"); + if (newWindow && newPrivateWindow) { + newPrivateWindow.hidden = true; + newWindow.label = newPrivateWindow.label; + newWindow.accessKey = newPrivateWindow.accessKey; + newWindow.command = newPrivateWindow.command; + } + } + }, +}; + +/** + * Switch to a tab that has a given URI, and focuses its browser window. + * If a matching tab is in this window, it will be switched to. Otherwise, other + * windows will be searched. + * + * @param aURI + * URI to search for + * @param aOpenNew + * True to open a new tab and switch to it, if no existing tab is found. + * If no suitable window is found, a new one will be opened. + * @param aOpenParams + * If switching to this URI results in us opening a tab, aOpenParams + * will be the parameter object that gets passed to openTrustedLinkIn. Please + * see the documentation for openTrustedLinkIn to see what parameters can be + * passed via this object. + * This object also allows: + * - 'ignoreFragment' property to be set to true to exclude fragment-portion + * matching when comparing URIs. + * If set to "whenComparing", the fragment will be unmodified. + * If set to "whenComparingAndReplace", the fragment will be replaced. + * - 'ignoreQueryString' boolean property to be set to true to exclude query string + * matching when comparing URIs. + * - 'replaceQueryString' boolean property to be set to true to exclude query string + * matching when comparing URIs and overwrite the initial query string with + * the one from the new URI. + * - 'adoptIntoActiveWindow' boolean property to be set to true to adopt the tab + * into the current window. + * @param aUserContextId + * If not null, will switch to the first found tab having the provided + * userContextId. + * @return True if an existing tab was found, false otherwise + */ +function switchToTabHavingURI( + aURI, + aOpenNew, + aOpenParams = {}, + aUserContextId = null +) { + // Certain URLs can be switched to irrespective of the source or destination + // window being in private browsing mode: + const kPrivateBrowsingWhitelist = new Set(["about:addons"]); + + let ignoreFragment = aOpenParams.ignoreFragment; + let ignoreQueryString = aOpenParams.ignoreQueryString; + let replaceQueryString = aOpenParams.replaceQueryString; + let adoptIntoActiveWindow = aOpenParams.adoptIntoActiveWindow; + + // These properties are only used by switchToTabHavingURI and should + // not be used as a parameter for the new load. + delete aOpenParams.ignoreFragment; + delete aOpenParams.ignoreQueryString; + delete aOpenParams.replaceQueryString; + delete aOpenParams.adoptIntoActiveWindow; + + let isBrowserWindow = !!window.gBrowser; + + // This will switch to the tab in aWindow having aURI, if present. + function switchIfURIInWindow(aWindow) { + // We can switch tab only if if both the source and destination windows have + // the same private-browsing status. + if ( + !kPrivateBrowsingWhitelist.has(aURI.spec) && + PrivateBrowsingUtils.isWindowPrivate(window) !== + PrivateBrowsingUtils.isWindowPrivate(aWindow) + ) { + return false; + } + + // Remove the query string, fragment, both, or neither from a given url. + function cleanURL(url, removeQuery, removeFragment) { + let ret = url; + if (removeFragment) { + ret = ret.split("#")[0]; + if (removeQuery) { + // This removes a query, if present before the fragment. + ret = ret.split("?")[0]; + } + } else if (removeQuery) { + // This is needed in case there is a fragment after the query. + let fragment = ret.split("#")[1]; + ret = ret + .split("?")[0] + .concat(fragment != undefined ? "#".concat(fragment) : ""); + } + return ret; + } + + // Need to handle nsSimpleURIs here too (e.g. about:...), which don't + // work correctly with URL objects - so treat them as strings + let ignoreFragmentWhenComparing = + typeof ignoreFragment == "string" && + ignoreFragment.startsWith("whenComparing"); + let requestedCompare = cleanURL( + aURI.displaySpec, + ignoreQueryString || replaceQueryString, + ignoreFragmentWhenComparing + ); + let browsers = aWindow.gBrowser.browsers; + for (let i = 0; i < browsers.length; i++) { + let browser = browsers[i]; + let browserCompare = cleanURL( + browser.currentURI.displaySpec, + ignoreQueryString || replaceQueryString, + ignoreFragmentWhenComparing + ); + let browserUserContextId = browser.getAttribute("usercontextid"); + if (aUserContextId != null && aUserContextId != browserUserContextId) { + continue; + } + if (requestedCompare == browserCompare) { + // If adoptIntoActiveWindow is set, and this is a cross-window switch, + // adopt the tab into the current window, after the active tab. + let doAdopt = + adoptIntoActiveWindow && isBrowserWindow && aWindow != window; + + if (doAdopt) { + const newTab = window.gBrowser.adoptTab( + aWindow.gBrowser.getTabForBrowser(browser), + window.gBrowser.tabContainer.selectedIndex + 1, + /* aSelectTab = */ true + ); + if (!newTab) { + doAdopt = false; + } + } + if (!doAdopt) { + aWindow.focus(); + } + + if (ignoreFragment == "whenComparingAndReplace" || replaceQueryString) { + browser.loadURI(aURI, { + triggeringPrincipal: + aOpenParams.triggeringPrincipal || + _createNullPrincipalFromTabUserContextId(), + }); + } + + if (!doAdopt) { + aWindow.gBrowser.tabContainer.selectedIndex = i; + } + + return true; + } + } + return false; + } + + // This can be passed either nsIURI or a string. + if (!(aURI instanceof Ci.nsIURI)) { + aURI = Services.io.newURI(aURI); + } + + // Prioritise this window. + if (isBrowserWindow && switchIfURIInWindow(window)) { + return true; + } + + for (let browserWin of browserWindows()) { + // Skip closed (but not yet destroyed) windows, + // and the current window (which was checked earlier). + if (browserWin.closed || browserWin == window) { + continue; + } + if (switchIfURIInWindow(browserWin)) { + return true; + } + } + + // No opened tab has that url. + if (aOpenNew) { + if ( + UrlbarPrefs.get("switchTabs.searchAllContainers") && + aUserContextId != null + ) { + aOpenParams.userContextId = aUserContextId; + } + if (isBrowserWindow && gBrowser.selectedTab.isEmpty) { + openTrustedLinkIn(aURI.spec, "current", aOpenParams); + } else { + openTrustedLinkIn(aURI.spec, "tab", aOpenParams); + } + } + + return false; +} + +var RestoreLastSessionObserver = { + init() { + if ( + SessionStore.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window) + ) { + Services.obs.addObserver(this, "sessionstore-last-session-cleared", true); + goSetCommandEnabled("Browser:RestoreLastSession", true); + } else if (SessionStore.willAutoRestore) { + document.getElementById("Browser:RestoreLastSession").hidden = true; + } + }, + + observe() { + // The last session can only be restored once so there's + // no way we need to re-enable our menu item. + Services.obs.removeObserver(this, "sessionstore-last-session-cleared"); + goSetCommandEnabled("Browser:RestoreLastSession", false); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), +}; + +/* Observes menus and adjusts their size for better + * usability when opened via a touch screen. */ +var MenuTouchModeObserver = { + init() { + window.addEventListener("popupshowing", this, true); + }, + + handleEvent(event) { + let target = event.originalTarget; + if (event.inputSource == MouseEvent.MOZ_SOURCE_TOUCH) { + target.setAttribute("touchmode", "true"); + } else { + target.removeAttribute("touchmode"); + } + }, + + uninit() { + window.removeEventListener("popupshowing", this, true); + }, +}; + +// Prompt user to restart the browser in safe mode +function safeModeRestart() { + if (Services.appinfo.inSafeMode) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + if (cancelQuit.data) { + return; + } + + Services.startup.quit( + Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit + ); + return; + } + + Services.obs.notifyObservers(window, "restart-in-safe-mode"); +} + +/* duplicateTabIn duplicates tab in a place specified by the parameter |where|. + * + * |where| can be: + * "tab" new tab + * "tabshifted" same as "tab" but in background if default is to select new + * tabs, and vice versa + * "window" new window + * + * delta is the offset to the history entry that you want to load. + */ +function duplicateTabIn(aTab, where, delta) { + switch (where) { + case "window": + let otherWin = OpenBrowserWindow({ + private: PrivateBrowsingUtils.isBrowserPrivate(aTab.linkedBrowser), + }); + let delayedStartupFinished = (subject, topic) => { + if ( + topic == "browser-delayed-startup-finished" && + subject == otherWin + ) { + Services.obs.removeObserver(delayedStartupFinished, topic); + let otherGBrowser = otherWin.gBrowser; + let otherTab = otherGBrowser.selectedTab; + SessionStore.duplicateTab(otherWin, aTab, delta); + otherGBrowser.removeTab(otherTab, { animate: false }); + } + }; + + Services.obs.addObserver( + delayedStartupFinished, + "browser-delayed-startup-finished" + ); + break; + case "tabshifted": + SessionStore.duplicateTab(window, aTab, delta); + // A background tab has been opened, nothing else to do here. + break; + case "tab": + SessionStore.duplicateTab(window, aTab, delta, true, { + inBackground: false, + }); + break; + } +} + +var MousePosTracker = { + _listeners: new Set(), + _x: 0, + _y: 0, + + /** + * Registers a listener. + * + * @param listener (object) + * A listener is expected to expose the following properties: + * + * getMouseTargetRect (function) + * Returns the rect that the MousePosTracker needs to alert + * the listener about if the mouse happens to be within it. + * + * onMouseEnter (function, optional) + * The function to be called if the mouse enters the rect + * returned by getMouseTargetRect. MousePosTracker always + * runs this inside of a requestAnimationFrame, since it + * assumes that the notification is used to update the DOM. + * + * onMouseLeave (function, optional) + * The function to be called if the mouse exits the rect + * returned by getMouseTargetRect. MousePosTracker always + * runs this inside of a requestAnimationFrame, since it + * assumes that the notification is used to update the DOM. + */ + addListener(listener) { + if (this._listeners.has(listener)) { + return; + } + + listener._hover = false; + this._listeners.add(listener); + + this._callListener(listener); + }, + + removeListener(listener) { + this._listeners.delete(listener); + }, + + handleEvent(event) { + this._x = event.screenX - window.mozInnerScreenX; + this._y = event.screenY - window.mozInnerScreenY; + + this._listeners.forEach(listener => { + try { + this._callListener(listener); + } catch (e) { + console.error(e); + } + }); + }, + + _callListener(listener) { + let rect = listener.getMouseTargetRect(); + let hover = + this._x >= rect.left && + this._x <= rect.right && + this._y >= rect.top && + this._y <= rect.bottom; + + if (hover == listener._hover) { + return; + } + + listener._hover = hover; + + if (hover) { + if (listener.onMouseEnter) { + listener.onMouseEnter(); + } + } else if (listener.onMouseLeave) { + listener.onMouseLeave(); + } + }, +}; + +var ToolbarIconColor = { + _windowState: { + active: false, + fullscreen: false, + tabsintitlebar: false, + }, + init() { + this._initialized = true; + + window.addEventListener("nativethemechange", this); + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); + window.addEventListener("toolbarvisibilitychange", this); + window.addEventListener("windowlwthemeupdate", this); + + // If the window isn't active now, we assume that it has never been active + // before and will soon become active such that inferFromText will be + // called from the initial activate event. + if (Services.focus.activeWindow == window) { + this.inferFromText("activate"); + } + }, + + uninit() { + this._initialized = false; + + window.removeEventListener("nativethemechange", this); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); + window.removeEventListener("toolbarvisibilitychange", this); + window.removeEventListener("windowlwthemeupdate", this); + }, + + handleEvent(event) { + switch (event.type) { + case "activate": + case "deactivate": + case "nativethemechange": + case "windowlwthemeupdate": + this.inferFromText(event.type); + break; + case "toolbarvisibilitychange": + this.inferFromText(event.type, event.visible); + break; + } + }, + + // a cache of luminance values for each toolbar + // to avoid unnecessary calls to getComputedStyle + _toolbarLuminanceCache: new Map(), + + inferFromText(reason, reasonValue) { + if (!this._initialized) { + return; + } + switch (reason) { + case "activate": // falls through + case "deactivate": + this._windowState.active = reason === "activate"; + break; + case "fullscreen": + this._windowState.fullscreen = reasonValue; + break; + case "nativethemechange": + case "windowlwthemeupdate": + // theme change, we'll need to recalculate all color values + this._toolbarLuminanceCache.clear(); + break; + case "toolbarvisibilitychange": + // toolbar changes dont require reset of the cached color values + break; + case "tabsintitlebar": + this._windowState.tabsintitlebar = reasonValue; + break; + } + + let toolbarSelector = ".browser-toolbar:not([collapsed=true])"; + if (AppConstants.platform == "macosx") { + toolbarSelector += ":not([type=menubar])"; + } + + // The getComputedStyle calls and setting the brighttext are separated in + // two loops to avoid flushing layout and making it dirty repeatedly. + let cachedLuminances = this._toolbarLuminanceCache; + let luminances = new Map(); + for (let toolbar of document.querySelectorAll(toolbarSelector)) { + // toolbars *should* all have ids, but guard anyway to avoid blowing up + let cacheKey = + toolbar.id && toolbar.id + JSON.stringify(this._windowState); + // lookup cached luminance value for this toolbar in this window state + let luminance = cacheKey && cachedLuminances.get(cacheKey); + if (isNaN(luminance)) { + let { r, g, b } = InspectorUtils.colorToRGBA( + getComputedStyle(toolbar).color + ); + luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b; + if (cacheKey) { + cachedLuminances.set(cacheKey, luminance); + } + } + luminances.set(toolbar, luminance); + } + + const luminanceThreshold = 127; // In between 0 and 255 + for (let [toolbar, luminance] of luminances) { + if (luminance <= luminanceThreshold) { + toolbar.removeAttribute("brighttext"); + } else { + toolbar.setAttribute("brighttext", "true"); + } + } + }, +}; + +var PanicButtonNotifier = { + init() { + this._initialized = true; + if (window.PanicButtonNotifierShouldNotify) { + delete window.PanicButtonNotifierShouldNotify; + this.notify(); + } + }, + createPanelIfNeeded() { + // Lazy load the panic-button-success-notification panel the first time we need to display it. + if (!document.getElementById("panic-button-success-notification")) { + let template = document.getElementById("panicButtonNotificationTemplate"); + template.replaceWith(template.content); + } + }, + notify() { + if (!this._initialized) { + window.PanicButtonNotifierShouldNotify = true; + return; + } + // Display notification panel here... + try { + this.createPanelIfNeeded(); + let popup = document.getElementById("panic-button-success-notification"); + popup.hidden = false; + // To close the popup in 3 seconds after the popup is shown but left uninteracted. + let onTimeout = () => { + PanicButtonNotifier.close(); + removeListeners(); + }; + popup.addEventListener("popupshown", function () { + PanicButtonNotifier.timer = setTimeout(onTimeout, 3000); + }); + // To prevent the popup from closing when user tries to interact with the + // popup using mouse or keyboard. + let onUserInteractsWithPopup = () => { + clearTimeout(PanicButtonNotifier.timer); + removeListeners(); + }; + popup.addEventListener("mouseover", onUserInteractsWithPopup); + window.addEventListener("keydown", onUserInteractsWithPopup); + let removeListeners = () => { + popup.removeEventListener("mouseover", onUserInteractsWithPopup); + window.removeEventListener("keydown", onUserInteractsWithPopup); + popup.removeEventListener("popuphidden", removeListeners); + }; + popup.addEventListener("popuphidden", removeListeners); + + let widget = CustomizableUI.getWidget("panic-button").forWindow(window); + let anchor = widget.anchor.icon; + popup.openPopup(anchor, popup.getAttribute("position")); + } catch (ex) { + console.error(ex); + } + }, + close() { + let popup = document.getElementById("panic-button-success-notification"); + popup.hidePopup(); + }, +}; + +const SafeBrowsingNotificationBox = { + _currentURIBaseDomain: null, + async show(title, buttons) { + let uri = gBrowser.currentURI; + + // start tracking host so that we know when we leave the domain + try { + this._currentURIBaseDomain = Services.eTLD.getBaseDomain(uri); + } catch (e) { + // If we can't get the base domain, fallback to use host instead. However, + // host is sometimes empty when the scheme is file. In this case, just use + // spec. + this._currentURIBaseDomain = uri.asciiHost || uri.asciiSpec; + } + + let notificationBox = gBrowser.getNotificationBox(); + let value = "blocked-badware-page"; + + let previousNotification = notificationBox.getNotificationWithValue(value); + if (previousNotification) { + notificationBox.removeNotification(previousNotification); + } + + let notification = await notificationBox.appendNotification( + value, + { + label: title, + image: "chrome://global/skin/icons/blocked.svg", + priority: notificationBox.PRIORITY_CRITICAL_HIGH, + }, + buttons + ); + // Persist the notification until the user removes so it + // doesn't get removed on redirects. + notification.persistence = -1; + }, + onLocationChange(aLocationURI) { + // take this to represent that you haven't visited a bad place + if (!this._currentURIBaseDomain) { + return; + } + + let newURIBaseDomain = Services.eTLD.getBaseDomain(aLocationURI); + + if (newURIBaseDomain !== this._currentURIBaseDomain) { + let notificationBox = gBrowser.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue( + "blocked-badware-page" + ); + if (notification) { + notificationBox.removeNotification(notification, false); + } + + this._currentURIBaseDomain = null; + } + }, +}; + +/** + * The TabDialogBox supports opening window dialogs as SubDialogs on the tab and content + * level. Both tab and content dialogs have their own separate managers. + * Dialogs will be queued FIFO and cover the web content. + * Dialogs are closed when the user reloads or leaves the page. + * While a dialog is open PopupNotifications, such as permission prompts, are + * suppressed. + */ +class TabDialogBox { + static _containerFor(browser) { + return browser.closest(".browserStack, .webextension-popup-stack"); + } + + constructor(browser) { + this._weakBrowserRef = Cu.getWeakReference(browser); + + // Create parent element for tab dialogs + let template = document.getElementById("dialogStackTemplate"); + let dialogStack = template.content.cloneNode(true).firstElementChild; + dialogStack.classList.add("tab-prompt-dialog"); + + TabDialogBox._containerFor(browser).appendChild(dialogStack); + + // Initially the stack only contains the template + let dialogTemplate = dialogStack.firstElementChild; + + // Create dialog manager for prompts at the tab level. + this._tabDialogManager = new SubDialogManager({ + dialogStack, + dialogTemplate, + orderType: SubDialogManager.ORDER_QUEUE, + allowDuplicateDialogs: true, + dialogOptions: { + consumeOutsideClicks: false, + }, + }); + } + + /** + * Open a dialog on tab or content level. + * @param {String} aURL - URL of the dialog to load in the tab box. + * @param {Object} [aOptions] + * @param {String} [aOptions.features] - Comma separated list of window + * features. + * @param {Boolean} [aOptions.allowDuplicateDialogs] - Whether to allow + * showing multiple dialogs with aURL at the same time. If false calls for + * duplicate dialogs will be dropped. + * @param {String} [aOptions.sizeTo] - Pass "available" to stretch dialog to + * roughly content size. Any max-width or max-height style values on the document root + * will also be applied to the dialog box. + * @param {Boolean} [aOptions.keepOpenSameOriginNav] - By default dialogs are + * aborted on any navigation. + * Set to true to keep the dialog open for same origin navigation. + * @param {Number} [aOptions.modalType] - The modal type to create the dialog for. + * By default, we show the dialog for tab prompts. + * @param {Boolean} [aOptions.hideContent] - When true, we are about to show a prompt that is requesting the + * users credentials for a toplevel load of a resource from a base domain different from the base domain of the currently loaded page. + * To avoid auth prompt spoofing (see bug 791594) we hide the current sites content + * (among other protection mechanisms, that are not handled here, see the bug for reference). + * @returns {Object} [result] Returns an object { closedPromise, dialog }. + * @returns {Promise} [result.closedPromise] Resolves once the dialog has been closed. + * @returns {SubDialog} [result.dialog] A reference to the opened SubDialog. + */ + open( + aURL, + { + features = null, + allowDuplicateDialogs = true, + sizeTo, + keepOpenSameOriginNav, + modalType = null, + allowFocusCheckbox = false, + hideContent = false, + } = {}, + ...aParams + ) { + let resolveClosed; + let closedPromise = new Promise(resolve => (resolveClosed = resolve)); + // Get the dialog manager to open the prompt with. + let dialogManager = + modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT + ? this.getContentDialogManager() + : this._tabDialogManager; + + let hasDialogs = () => + this._tabDialogManager.hasDialogs || + this._contentDialogManager?.hasDialogs; + + if (!hasDialogs()) { + this._onFirstDialogOpen(); + } + + let closingCallback = event => { + if (!hasDialogs()) { + this._onLastDialogClose(); + } + + if (allowFocusCheckbox && !event.detail?.abort) { + this.maybeSetAllowTabSwitchPermission(event.target); + } + }; + + if (modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT) { + sizeTo = "limitheight"; + } + + // Open dialog and resolve once it has been closed + let dialog = dialogManager.open( + aURL, + { + features, + allowDuplicateDialogs, + sizeTo, + closingCallback, + closedCallback: resolveClosed, + hideContent, + }, + ...aParams + ); + + // Marking the dialog externally, instead of passing it as an option. + // The SubDialog(Manager) does not care about navigation. + // dialog can be null here if allowDuplicateDialogs = false. + if (dialog) { + dialog._keepOpenSameOriginNav = keepOpenSameOriginNav; + } + return { closedPromise, dialog }; + } + + _onFirstDialogOpen() { + // Hide PopupNotifications to prevent them from covering up dialogs. + this.browser.setAttribute("tabDialogShowing", true); + UpdatePopupNotificationsVisibility(); + + // Register listeners + this._lastPrincipal = this.browser.contentPrincipal; + this.browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); + + this.tab?.addEventListener("TabClose", this); + } + + _onLastDialogClose() { + // Show PopupNotifications again. + this.browser.removeAttribute("tabDialogShowing"); + UpdatePopupNotificationsVisibility(); + + // Clean up listeners + this.browser.removeProgressListener(this); + this._lastPrincipal = null; + + this.tab?.removeEventListener("TabClose", this); + } + + _buildContentPromptDialog() { + let template = document.getElementById("dialogStackTemplate"); + let contentDialogStack = template.content.cloneNode(true).firstElementChild; + contentDialogStack.classList.add("content-prompt-dialog"); + + // Create a dialog manager for content prompts. + let browserContainer = TabDialogBox._containerFor(this.browser); + let tabPromptDialog = browserContainer.querySelector(".tab-prompt-dialog"); + browserContainer.insertBefore(contentDialogStack, tabPromptDialog); + + let contentDialogTemplate = contentDialogStack.firstElementChild; + this._contentDialogManager = new SubDialogManager({ + dialogStack: contentDialogStack, + dialogTemplate: contentDialogTemplate, + orderType: SubDialogManager.ORDER_QUEUE, + allowDuplicateDialogs: true, + dialogOptions: { + consumeOutsideClicks: false, + }, + }); + } + + handleEvent(event) { + if (event.type !== "TabClose") { + return; + } + this.abortAllDialogs(); + } + + abortAllDialogs() { + this._tabDialogManager.abortDialogs(); + this._contentDialogManager?.abortDialogs(); + } + + focus() { + // Prioritize focusing the dialog manager for tab prompts + if (this._tabDialogManager._dialogs.length) { + this._tabDialogManager.focusTopDialog(); + return; + } + this._contentDialogManager?.focusTopDialog(); + } + + /** + * If the user navigates away or refreshes the page, close all dialogs for + * the current browser. + */ + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + if ( + !aWebProgress.isTopLevel || + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT + ) { + return; + } + + // Dialogs can be exempt from closing on same origin location change. + let filterFn; + + // Test for same origin location change + if ( + this._lastPrincipal?.isSameOrigin( + aLocation, + this.browser.browsingContext.usePrivateBrowsing + ) + ) { + filterFn = dialog => !dialog._keepOpenSameOriginNav; + } + + this._lastPrincipal = this.browser.contentPrincipal; + + this._tabDialogManager.abortDialogs(filterFn); + this._contentDialogManager?.abortDialogs(filterFn); + } + + get tab() { + return gBrowser.getTabForBrowser(this.browser); + } + + get browser() { + let browser = this._weakBrowserRef.get(); + if (!browser) { + throw new Error("Stale dialog box! The associated browser is gone."); + } + return browser; + } + + getTabDialogManager() { + return this._tabDialogManager; + } + + getContentDialogManager() { + if (!this._contentDialogManager) { + this._buildContentPromptDialog(); + } + return this._contentDialogManager; + } + + onNextPromptShowAllowFocusCheckboxFor(principal) { + this._allowTabFocusByPromptPrincipal = principal; + } + + /** + * Sets the "focus-tab-by-prompt" permission for the dialog. + */ + maybeSetAllowTabSwitchPermission(dialog) { + let checkbox = dialog.querySelector("checkbox"); + + if (checkbox.checked) { + Services.perms.addFromPrincipal( + this._allowTabFocusByPromptPrincipal, + "focus-tab-by-prompt", + Services.perms.ALLOW_ACTION + ); + } + + // Don't show the "allow tab switch checkbox" for subsequent prompts. + this._allowTabFocusByPromptPrincipal = null; + } +} + +TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", +]); + +function TabModalPromptBox(browser) { + this._weakBrowserRef = Cu.getWeakReference(browser); + /* + * These WeakMaps holds the TabModalPrompt instances, key to the <tabmodalprompt> prompt + * in the DOM. We don't want to hold the instances directly to avoid leaking. + * + * WeakMap also prevents us from reading back its insertion order. + * Order of the elements in the DOM should be the only order to consider. + */ + this._contentPrompts = new WeakMap(); + this._tabPrompts = new WeakMap(); +} + +TabModalPromptBox.prototype = { + _promptCloseCallback( + onCloseCallback, + principalToAllowFocusFor, + allowFocusCheckbox, + ...args + ) { + if ( + principalToAllowFocusFor && + allowFocusCheckbox && + allowFocusCheckbox.checked + ) { + Services.perms.addFromPrincipal( + principalToAllowFocusFor, + "focus-tab-by-prompt", + Services.perms.ALLOW_ACTION + ); + } + onCloseCallback.apply(this, args); + }, + + getPrompt(promptEl) { + if (promptEl.classList.contains("tab-prompt")) { + return this._tabPrompts.get(promptEl); + } + return this._contentPrompts.get(promptEl); + }, + + appendPrompt(args, onCloseCallback) { + let browser = this.browser; + let newPrompt = new TabModalPrompt(browser.ownerGlobal); + + if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { + newPrompt.element.classList.add("tab-prompt"); + this._tabPrompts.set(newPrompt.element, newPrompt); + } else { + newPrompt.element.classList.add("content-prompt"); + this._contentPrompts.set(newPrompt.element, newPrompt); + } + + browser.parentNode.insertBefore( + newPrompt.element, + browser.nextElementSibling + ); + browser.setAttribute("tabmodalPromptShowing", true); + + // Indicate if a tab modal chrome prompt is being shown so that + // PopupNotifications are suppressed. + if ( + args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && + !browser.hasAttribute("tabmodalChromePromptShowing") + ) { + browser.setAttribute("tabmodalChromePromptShowing", true); + // Notify popup notifications of the UI change so they hide their + // notification panels. + UpdatePopupNotificationsVisibility(); + } + + let prompts = this.listPrompts(args.modalType); + if (prompts.length > 1) { + // Let's hide ourself behind the current prompt. + newPrompt.element.hidden = true; + } + + let principalToAllowFocusFor = this._allowTabFocusByPromptPrincipal; + delete this._allowTabFocusByPromptPrincipal; + + let allowFocusCheckbox; // Define outside the if block so we can bind it into the callback. + let hostForAllowFocusCheckbox = ""; + try { + hostForAllowFocusCheckbox = principalToAllowFocusFor.URI.host; + } catch (ex) { + /* Ignore exceptions for host-less URIs */ + } + if (hostForAllowFocusCheckbox) { + let allowFocusRow = document.createElement("div"); + + let spacer = document.createElement("div"); + allowFocusRow.appendChild(spacer); + + allowFocusCheckbox = document.createXULElement("checkbox"); + document.l10n.setAttributes( + allowFocusCheckbox, + "tabbrowser-allow-dialogs-to-get-focus", + { domain: hostForAllowFocusCheckbox } + ); + allowFocusRow.appendChild(allowFocusCheckbox); + + newPrompt.ui.rows.append(allowFocusRow); + } + + let tab = gBrowser.getTabForBrowser(browser); + let closeCB = this._promptCloseCallback.bind( + null, + onCloseCallback, + principalToAllowFocusFor, + allowFocusCheckbox + ); + newPrompt.init(args, tab, closeCB); + return newPrompt; + }, + + removePrompt(aPrompt) { + let { modalType } = aPrompt.args; + if (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { + this._tabPrompts.delete(aPrompt.element); + } else { + this._contentPrompts.delete(aPrompt.element); + } + + let browser = this.browser; + aPrompt.element.remove(); + + let prompts = this.listPrompts(modalType); + if (prompts.length) { + let prompt = prompts[prompts.length - 1]; + prompt.element.hidden = false; + // Because we were hidden before, this won't have been possible, so do it now: + prompt.Dialog.setDefaultFocus(); + } else if (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { + // If we remove the last tab chrome prompt, also remove the browser + // attribute. + browser.removeAttribute("tabmodalChromePromptShowing"); + // Notify popup notifications of the UI change so they show notification + // panels again. + UpdatePopupNotificationsVisibility(); + } + // Check if all prompts are closed + if (!this._hasPrompts()) { + browser.removeAttribute("tabmodalPromptShowing"); + browser.focus(); + } + }, + + /** + * Checks if the prompt box has prompt elements. + * @returns {Boolean} - true if there are prompt elements. + */ + _hasPrompts() { + return !!this._getPromptElements().length; + }, + + /** + * Get list of current prompt elements. + * @param {Number} [aModalType] - Optionally filter by + * Ci.nsIPrompt.MODAL_TYPE_. + * @returns {NodeList} - A list of tabmodalprompt elements. + */ + _getPromptElements(aModalType = null) { + let selector = "tabmodalprompt"; + + if (aModalType != null) { + if (aModalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { + selector += ".tab-prompt"; + } else { + selector += ".content-prompt"; + } + } + return this.browser.parentNode.querySelectorAll(selector); + }, + + /** + * Get a list of all TabModalPrompt objects associated with the prompt box. + * @param {Number} [aModalType] - Optionally filter by + * Ci.nsIPrompt.MODAL_TYPE_. + * @returns {TabModalPrompt[]} - An array of TabModalPrompt objects. + */ + listPrompts(aModalType = null) { + // Get the nodelist, then return the TabModalPrompt instances as an array + let promptMap; + + if (aModalType) { + if (aModalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { + promptMap = this._tabPrompts; + } else { + promptMap = this._contentPrompts; + } + } + + let elements = this._getPromptElements(aModalType); + + if (promptMap) { + return [...elements].map(el => promptMap.get(el)); + } + return [...elements].map( + el => this._contentPrompts.get(el) || this._tabPrompts.get(el) + ); + }, + + onNextPromptShowAllowFocusCheckboxFor(principal) { + this._allowTabFocusByPromptPrincipal = principal; + }, + + get browser() { + let browser = this._weakBrowserRef.get(); + if (!browser) { + throw new Error("Stale promptbox! The associated browser is gone."); + } + return browser; + }, +}; + +// Handle window-modal prompts that we want to display with the same style as +// tab-modal prompts. +var gDialogBox = { + _dialog: null, + _nextOpenJumpsQueue: false, + _queued: [], + + // Used to wait for a `close` event from the HTML + // dialog. The event is fired asynchronously, which means + // that if we open another dialog immediately after the + // previous one, we might be confused into thinking a + // `close` event for the old dialog is for the new one. + // As they have the same event target, we have no way of + // distinguishing them. So we wait for the `close` event + // to have happened before allowing another dialog to open. + _didCloseHTMLDialog: null, + // Whether we managed to open the dialog we tried to open. + // Used to avoid waiting for the above callback in case + // of an error opening the dialog. + _didOpenHTMLDialog: false, + + get dialog() { + return this._dialog; + }, + + get isOpen() { + return !!this._dialog; + }, + + replaceDialogIfOpen() { + this._dialog?.close(); + this._nextOpenJumpsQueue = true; + }, + + async open(uri, args) { + // If we need to queue, some callers indicate they should go first. + const queueMethod = this._nextOpenJumpsQueue ? "unshift" : "push"; + this._nextOpenJumpsQueue = false; + + // If we already have a dialog opened and are trying to open another, + // queue the next one to be opened later. + if (this.isOpen) { + return new Promise((resolve, reject) => { + this._queued[queueMethod]({ resolve, reject, uri, args }); + }); + } + + // We're not open. If we're in a modal state though, we can't + // show the dialog effectively. To avoid hanging by deadlock, + // just return immediately for sync prompts: + if (window.windowUtils.isInModalState() && !args.getProperty("async")) { + throw Components.Exception( + "Prompt could not be shown.", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + // Indicate if we should wait for the dialog to close. + this._didOpenHTMLDialog = false; + let haveClosedPromise = new Promise(resolve => { + this._didCloseHTMLDialog = resolve; + }); + + // Bring the window to the front in case we're minimized or occluded: + window.focus(); + + try { + await this._open(uri, args); + } catch (ex) { + console.error(ex); + } finally { + let dialog = document.getElementById("window-modal-dialog"); + if (dialog.open) { + dialog.close(); + } + // If the dialog was opened successfully, then we can wait for it + // to close before trying to open any others. + if (this._didOpenHTMLDialog) { + await haveClosedPromise; + } + dialog.style.visibility = "hidden"; + dialog.style.height = "0"; + dialog.style.width = "0"; + document.documentElement.removeAttribute("window-modal-open"); + dialog.removeEventListener("dialogopen", this); + dialog.removeEventListener("close", this); + this._updateMenuAndCommandState(true /* to enable */); + this._dialog = null; + UpdatePopupNotificationsVisibility(); + } + if (this._queued.length) { + setTimeout(() => this._openNextDialog(), 0); + } + return args; + }, + + _openNextDialog() { + if (!this.isOpen) { + let { resolve, reject, uri, args } = this._queued.shift(); + this.open(uri, args).then(resolve, reject); + } + }, + + handleEvent(event) { + switch (event.type) { + case "dialogopen": + this._dialog.focus(true); + break; + case "close": + this._didCloseHTMLDialog(); + this._dialog.close(); + break; + } + }, + + _open(uri, args) { + // Get this offset before we touch style below, as touching style seems + // to reset the cached layout bounds. + let offset = window.windowUtils.getBoundsWithoutFlushing( + gBrowser.selectedBrowser + ).top; + let parentElement = document.getElementById("window-modal-dialog"); + parentElement.style.setProperty("--chrome-offset", offset + "px"); + parentElement.style.removeProperty("visibility"); + parentElement.style.removeProperty("width"); + parentElement.style.removeProperty("height"); + document.documentElement.setAttribute("window-modal-open", true); + // Call this first so the contents show up and get layout, which is + // required for SubDialog to work. + parentElement.showModal(); + this._didOpenHTMLDialog = true; + + // Disable menus and shortcuts. + this._updateMenuAndCommandState(false /* to disable */); + + // Now actually set up the dialog contents: + let template = document.getElementById("window-modal-dialog-template") + .content.firstElementChild; + parentElement.addEventListener("dialogopen", this); + parentElement.addEventListener("close", this); + this._dialog = new SubDialog({ + template, + parentElement, + id: "window-modal-dialog-subdialog", + options: { + consumeOutsideClicks: false, + }, + }); + let closedPromise = new Promise(resolve => { + this._closedCallback = function () { + PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed"); + resolve(); + }; + }); + this._dialog.open( + uri, + { + features: "resizable=no", + modalType: Ci.nsIPrompt.MODAL_TYPE_INTERNAL_WINDOW, + closedCallback: () => { + this._closedCallback(); + }, + }, + args + ); + UpdatePopupNotificationsVisibility(); + return closedPromise; + }, + + _nonUpdatableElements: new Set([ + // Make an exception for debugging tools, for developer ease of use. + "key_browserConsole", + "key_browserToolbox", + + // Don't touch the editing keys/commands which we might want inside the dialog. + "key_undo", + "key_redo", + + "key_cut", + "key_copy", + "key_paste", + "key_delete", + "key_selectAll", + ]), + + _updateMenuAndCommandState(shouldBeEnabled) { + let editorCommands = document.getElementById("editMenuCommands"); + // For the following items, set or clear disabled state: + // - toplevel menubar items (will affect inner items on macOS) + // - command elements + // - key elements not connected to command elements. + for (let element of document.querySelectorAll( + "menubar > menu, command, key:not([command])" + )) { + if ( + editorCommands?.contains(element) || + (element.id && this._nonUpdatableElements.has(element.id)) + ) { + continue; + } + if (element.nodeName == "key" && element.command) { + continue; + } + if (!shouldBeEnabled) { + if (element.getAttribute("disabled") != "true") { + element.setAttribute("disabled", true); + } else { + element.setAttribute("wasdisabled", true); + } + } else if (element.getAttribute("wasdisabled") != "true") { + element.removeAttribute("disabled"); + } else { + element.removeAttribute("wasdisabled"); + } + } + }, +}; + +// browser.js loads in the library window, too, but we can only show prompts +// in the main browser window: +if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + gDialogBox = null; +} + +var ConfirmationHint = { + _timerID: null, + + /** + * Shows a transient, non-interactive confirmation hint anchored to an + * element, usually used in response to a user action to reaffirm that it was + * successful and potentially provide extra context. Examples for such hints: + * - "Saved to bookmarks" after bookmarking a page + * - "Sent!" after sending a tab to another device + * - "Queued (offline)" when attempting to send a tab to another device + * while offline + * + * @param anchor (DOM node, required) + * The anchor for the panel. + * @param messageId (string, required) + * For getting the message string from confirmationHints.ftl + * @param options (object, optional) + * An object with the following optional properties: + * - event (DOM event): The event that triggered the feedback + * - descriptionId (string): message ID of the description text + * - position (string): position of the panel relative to the anchor. + * + */ + show(anchor, messageId, options = {}) { + this._reset(); + + MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl"); + MozXULElement.insertFTLIfNeeded("browser/confirmationHints.ftl"); + document.l10n.setAttributes(this._message, messageId); + + if (options.descriptionId) { + document.l10n.setAttributes(this._description, options.descriptionId); + this._description.hidden = false; + this._panel.classList.add("with-description"); + } else { + this._description.hidden = true; + this._panel.classList.remove("with-description"); + } + + this._panel.setAttribute("data-message-id", messageId); + + // The timeout value used here allows the panel to stay open for + // 3s after the text transition (duration=120ms) has finished. + // If there is a description, we show for 6s after the text transition. + const DURATION = options.showDescription ? 6000 : 3000; + this._panel.addEventListener( + "popupshown", + () => { + this._animationBox.setAttribute("animate", "true"); + this._timerID = setTimeout(() => { + this._panel.hidePopup(true); + }, DURATION + 120); + }, + { once: true } + ); + + this._panel.addEventListener( + "popuphidden", + () => { + // reset the timerId in case our timeout wasn't the cause of the popup being hidden + this._reset(); + }, + { once: true } + ); + + this._panel.openPopup(anchor, { + position: options.position ?? "bottomleft topleft", + triggerEvent: options.event, + }); + }, + + _reset() { + if (this._timerID) { + clearTimeout(this._timerID); + this._timerID = null; + } + if (this.__panel) { + this._animationBox.removeAttribute("animate"); + this._panel.removeAttribute("data-message-id"); + } + }, + + get _panel() { + this._ensurePanel(); + return this.__panel; + }, + + get _animationBox() { + this._ensurePanel(); + delete this._animationBox; + return (this._animationBox = document.getElementById( + "confirmation-hint-checkmark-animation-container" + )); + }, + + get _message() { + this._ensurePanel(); + delete this._message; + return (this._message = document.getElementById( + "confirmation-hint-message" + )); + }, + + get _description() { + this._ensurePanel(); + delete this._description; + return (this._description = document.getElementById( + "confirmation-hint-description" + )); + }, + + _ensurePanel() { + if (!this.__panel) { + let wrapper = document.getElementById("confirmation-hint-wrapper"); + wrapper.replaceWith(wrapper.content); + this.__panel = document.getElementById("confirmation-hint"); + } + }, +}; + +var FirefoxViewHandler = { + tab: null, + BUTTON_ID: "firefox-view-button", + get button() { + return document.getElementById(this.BUTTON_ID); + }, + init() { + CustomizableUI.addListener(this); + + ChromeUtils.defineESModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", + }); + Services.obs.addObserver(this, "firefoxview-notification-dot-update"); + }, + uninit() { + CustomizableUI.removeListener(this); + Services.obs.removeObserver(this, "firefoxview-notification-dot-update"); + }, + onWidgetRemoved(aWidgetId) { + if (aWidgetId == this.BUTTON_ID && this.tab) { + gBrowser.removeTab(this.tab); + } + }, + onWidgetAdded(aWidgetId) { + if (aWidgetId === this.BUTTON_ID) { + this.button.removeAttribute("open"); + } + }, + openTab(section) { + if (!CustomizableUI.getPlacementOfWidget(this.BUTTON_ID)) { + CustomizableUI.addWidgetToArea( + this.BUTTON_ID, + CustomizableUI.AREA_TABSTRIP, + CustomizableUI.getPlacementOfWidget("tabbrowser-tabs").position + ); + } + let viewURL = "about:firefoxview"; + if (section) { + viewURL = `${viewURL}#${section}`; + } + // Need to account for navigation to Firefox View pages + if ( + this.tab && + this.tab.linkedBrowser.currentURI.spec.split("#")[0] != viewURL + ) { + gBrowser.removeTab(this.tab); + this.tab = null; + } + if (!this.tab) { + this.tab = gBrowser.addTrustedTab(viewURL); + this.tab.addEventListener("TabClose", this, { once: true }); + gBrowser.tabContainer.addEventListener("TabSelect", this); + window.addEventListener("activate", this); + gBrowser.hideTab(this.tab); + this.button.setAttribute("aria-controls", this.tab.linkedPanel); + } + // we put this here to avoid a race condition that would occur + // if this was called in response to "TabSelect" + this._closeDeviceConnectedTab(); + gBrowser.selectedTab = this.tab; + }, + openToolbarMouseEvent(event, section) { + if (event?.type == "mousedown" && event?.button != 0) { + return; + } + this.openTab(section); + }, + handleEvent(e) { + switch (e.type) { + case "TabSelect": + const selected = e.target == this.tab; + this.button?.toggleAttribute("open", selected); + this.button?.setAttribute("aria-pressed", selected); + this._recordViewIfTabSelected(); + this._onTabForegrounded(); + // If Fx View is opened, add temporary style to make first available tab focusable + // When Fx View is closed, remove temporary -moz-user-focus style from first available tab + gBrowser.visibleTabs[0].style.MozUserFocus = + e.target == this.tab ? "normal" : ""; + break; + case "TabClose": + this.tab = null; + gBrowser.tabContainer.removeEventListener("TabSelect", this); + this.button?.removeAttribute("aria-controls"); + break; + case "activate": + this._onTabForegrounded(); + break; + } + }, + observe(sub, topic, data) { + switch (topic) { + case "firefoxview-notification-dot-update": + let shouldShow = data === "true"; + this._toggleNotificationDot(shouldShow); + break; + } + }, + _closeDeviceConnectedTab() { + if (!TabsSetupFlowManager.didFxaTabOpen) { + return; + } + // close the tab left behind after a user pairs a device and + // is redirected back to the Firefox View tab + const fxaRoot = Services.prefs.getCharPref( + "identity.fxaccounts.remote.root" + ); + const fxDeviceConnectedTab = gBrowser.tabs.find(tab => + tab.linkedBrowser.currentURI.displaySpec.startsWith( + `${fxaRoot}pair/auth/complete` + ) + ); + + if (!fxDeviceConnectedTab) { + return; + } + + if (gBrowser.tabs.length <= 2) { + // if its the only tab besides the Firefox View tab, + // open a new tab first so the browser doesn't close + gBrowser.addTrustedTab("about:newtab"); + } + gBrowser.removeTab(fxDeviceConnectedTab); + TabsSetupFlowManager.didFxaTabOpen = false; + }, + _onTabForegrounded() { + if (this.tab?.selected) { + this.SyncedTabs.syncTabs(); + Services.obs.notifyObservers( + null, + "firefoxview-notification-dot-update", + "false" + ); + } + }, + _recordViewIfTabSelected() { + if (this.tab?.selected) { + const PREF_NAME = "browser.firefox-view.view-count"; + const MAX_VIEW_COUNT = 10; + let viewCount = Services.prefs.getIntPref(PREF_NAME, 0); + + // Record telemetry + Services.telemetry.setEventRecordingEnabled("firefoxview_next", true); + Services.telemetry.recordEvent( + "firefoxview_next", + "tab_selected", + "toolbarbutton", + null, + {} + ); + + if (viewCount < MAX_VIEW_COUNT) { + Services.prefs.setIntPref(PREF_NAME, viewCount + 1); + } + } + }, + _toggleNotificationDot(shouldShow) { + this.button?.toggleAttribute("attention", shouldShow); + }, +}; |