/* -*- 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/MemoryNotificationDB.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",
  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",
  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",
  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,
  ["BrowserCommands", "kSkipCacheFlags"],
  "chrome://browser/content/browser-commands.js"
);

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,
  "SelectTranslationsPanel",
  "chrome://browser/content/translations/selectTranslationsPanel.js"
);
XPCOMUtils.defineLazyScriptGetter(
  this,
  "FullPageTranslationsPanel",
  "chrome://browser/content/translations/fullPageTranslationsPanel.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"
);
XPCOMUtils.defineLazyScriptGetter(
  this,
  "gProfiles",
  "chrome://browser/content/browser-profiles.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,
  "gFxaToolbarEnabled",
  "identity.fxaccounts.toolbar.enabled",
  false,
  (aPref, aOldVal, aNewVal) => {
    updateFxaToolbarMenu(aNewVal);
  }
);

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "gFxaToolbarAccessed",
  "identity.fxaccounts.toolbar.accessed",
  false,
  () => {
    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("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",
  "about:profilemanager",
];

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() {
  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) {
    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) {
    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() {
          // 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) {
    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,
  });
}

function HandleAppCommandEvent(evt) {
  switch (evt.command) {
    case "Back":
      BrowserCommands.back();
      break;
    case "Forward":
      BrowserCommands.forward();
      break;
    case "Reload":
      BrowserCommands.reloadSkipCache();
      break;
    case "Stop":
      if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") {
        BrowserCommands.stop();
      }
      break;
    case "Search":
      BrowserSearch.webSearch();
      break;
    case "Bookmarks":
      SidebarController.toggle("viewBookmarksSidebar");
      break;
    case "Home":
      BrowserCommands.home();
      break;
    case "New":
      BrowserCommands.openTab();
      break;
    case "Close":
      BrowserCommands.closeTabOrWindow();
      break;
    case "Find":
      gLazyFindCommand("onFindCommand");
      break;
    case "Help":
      openHelpLink("firefox-help");
      break;
    case "Open":
      BrowserCommands.openFileWindow();
      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 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
  );
}

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 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;
}

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;
}

// 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) {
          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 = BrowserUtils.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 BrowserCommands.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 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 = {}) {
  let telemetryObj = {};
  TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj);

  let win = BrowserWindowTracker.openWindow({
    openerWindow: window,
    ...options,
  });

  win.addEventListener(
    "MozAfterPaint",
    () => {
      TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
    },
    { once: true }
  );

  return win;
}

/**
 * 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() {
    // 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.isFullPageTranslationsRestrictedForPage(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.BrowserCommands.openTab(); // 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;
  }

  // triggerNode can be a nested child element of a toolbaritem.
  let toolbarItem = popup.triggerNode;
  while (toolbarItem) {
    let localName = toolbarItem.localName;
    if (localName == "toolbar") {
      toolbarItem = null;
      break;
    }
    if (localName == "toolbarpaletteitem") {
      toolbarItem = toolbarItem.firstElementChild;
      break;
    }
    if (localName == "menupopup") {
      aEvent.preventDefault();
      aEvent.stopPropagation();
      return;
    }
    let parent = toolbarItem.parentElement;
    if (parent) {
      if (
        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;
  }

  // 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;
  }

  let showTabStripItems = 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?.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() {
  BrowserCommands.pageInfo(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 =
      SidebarController.initialized && SidebarController.isOpen;
    if (shouldUpdateSidebar) {
      docs.push(SidebarController.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 = SidebarController.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 = BrowserUtils.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 = BrowserUtils.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();
  }
}

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) {
    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 == "attestation-consent") {
      this.attestation_consent(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() {
          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
    );
  },

  attestation_consent(mgr, { origin, tid }) {
    let mainAction = {
      label: gNavigatorBundle.getString("webauthn.allow"),
      accessKey: gNavigatorBundle.getString("webauthn.allow.accesskey"),
      callback(_state) {
        mgr.setHasAttestationConsent(tid, true);
      },
    };
    let secondaryActions = [
      {
        label: gNavigatorBundle.getString("webauthn.block"),
        accessKey: gNavigatorBundle.getString("webauthn.block.accesskey"),
        callback(_state) {
          mgr.setHasAttestationConsent(tid, false);
        },
      },
    ];

    let learnMoreURL =
      Services.urlFormatter.formatURLPref("app.support.baseURL") +
      "webauthn-direct-attestation";

    let options = {
      learnMoreURL,
      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();
    }
  },

  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()
      );
    }
  },
};

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() {
    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",
]);

// 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.
   *         - l10nArgs (object): l10n arguments for the messageId.
   */
  show(anchor, messageId, options = {}) {
    this._reset();

    MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
    MozXULElement.insertFTLIfNeeded("browser/confirmationHints.ftl");
    document.l10n.setAttributes(this._message, messageId, options.l10nArgs);
    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",
    });
  },
  uninit() {
    CustomizableUI.removeListener(this);
  },
  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;
    }
  },
  _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();
    }
  },
  _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);
      }
    }
  },
};
