/* 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/. */

"use strict";

// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
  const { AppConstants } = ChromeUtils.importESModule(
    "resource://gre/modules/AppConstants.sys.mjs"
  );

  const { XPCOMUtils } = ChromeUtils.importESModule(
    "resource://gre/modules/XPCOMUtils.sys.mjs"
  );

  let lazy = {};

  ChromeUtils.defineESModuleGetters(lazy, {
    BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
    Finder: "resource://gre/modules/Finder.sys.mjs",
    FinderParent: "resource://gre/modules/FinderParent.sys.mjs",
    PopupBlocker: "resource://gre/actors/PopupBlockingParent.sys.mjs",
    SelectParentHelper: "resource://gre/actors/SelectParent.sys.mjs",
    RemoteWebNavigation: "resource://gre/modules/RemoteWebNavigation.sys.mjs",
  });

  ChromeUtils.defineLazyGetter(lazy, "blankURI", () =>
    Services.io.newURI("about:blank")
  );

  XPCOMUtils.defineLazyServiceGetter(
    lazy,
    "contentAnalysis",
    "@mozilla.org/contentanalysis;1",
    Ci.nsIContentAnalysis
  );

  let lazyPrefs = {};
  XPCOMUtils.defineLazyPreferenceGetter(
    lazyPrefs,
    "unloadTimeoutMs",
    "dom.beforeunload_timeout_ms"
  );
  XPCOMUtils.defineLazyPreferenceGetter(
    lazyPrefs,
    "_contentAnalysisDragDropEnabled",
    "browser.contentanalysis.interception_point.drag_and_drop.enabled",
    true
  );

  Object.defineProperty(lazy, "ProcessHangMonitor", {
    configurable: true,
    get() {
      // Import if we can - this is a browser/ module so it may not be
      // available, in which case we return null. We replace this getter
      // when the module becomes available (should be on delayed startup
      // when the first browser window loads, via BrowserGlue.sys.mjs).
      const kURL = "resource:///modules/ProcessHangMonitor.sys.mjs";
      if (Cu.isESModuleLoaded(kURL)) {
        let { ProcessHangMonitor } = ChromeUtils.importESModule(kURL);
        // eslint-disable-next-line mozilla/valid-lazy
        Object.defineProperty(lazy, "ProcessHangMonitor", {
          value: ProcessHangMonitor,
        });
        return ProcessHangMonitor;
      }
      return null;
    },
  });

  // Get SessionStore module in the same as ProcessHangMonitor above.
  Object.defineProperty(lazy, "SessionStore", {
    configurable: true,
    get() {
      const kURL = "resource:///modules/sessionstore/SessionStore.sys.mjs";
      if (Cu.isESModuleLoaded(kURL)) {
        let { SessionStore } = ChromeUtils.importESModule(kURL);
        // eslint-disable-next-line mozilla/valid-lazy
        Object.defineProperty(lazy, "SessionStore", {
          value: SessionStore,
        });
        return SessionStore;
      }
      return null;
    },
  });

  const elementsToDestroyOnUnload = new Set();

  window.addEventListener(
    "unload",
    () => {
      for (let element of elementsToDestroyOnUnload.values()) {
        element.destroy();
      }
      elementsToDestroyOnUnload.clear();
    },
    { mozSystemGroup: true, once: true }
  );

  class MozBrowser extends MozElements.MozElementMixin(XULFrameElement) {
    static get observedAttributes() {
      return ["remote"];
    }

    constructor() {
      super();

      this.onPageHide = this.onPageHide.bind(this);

      this.isNavigating = false;

      this._documentURI = null;
      this._characterSet = null;
      this._documentContentType = null;

      this._inPermitUnload = new WeakSet();

      this._originalURI = null;
      this._searchTerms = "";
      // When we open a prompt in reaction to a 401, if this 401 comes from
      // a different base domain, the url of that site will be stored here
      // and will be used for auth prompt spoofing protections.
      // See bug 791594 for reference.
      this._currentAuthPromptURI = null;
      /**
       * These are managed by the tabbrowser:
       */
      this.droppedLinkHandler = null;
      this.mIconURL = null;
      this.lastURI = null;

      ChromeUtils.defineLazyGetter(this, "popupBlocker", () => {
        return new lazy.PopupBlocker(this);
      });

      this.addEventListener(
        "dragover",
        event => {
          if (!this.droppedLinkHandler || event.defaultPrevented) {
            return;
          }

          // For drags that appear to be internal text (for example, tab drags),
          // set the dropEffect to 'none'. This prevents the drop even if some
          // other listener cancelled the event.
          var types = event.dataTransfer.types;
          if (
            types.includes("text/x-moz-text-internal") &&
            !types.includes("text/plain")
          ) {
            event.dataTransfer.dropEffect = "none";
            event.stopPropagation();
            event.preventDefault();
          }

          // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process
          // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
          if (this.isRemoteBrowser) {
            return;
          }

          let linkHandler = Services.droppedLinkHandler;
          if (linkHandler.canDropLink(event, false)) {
            event.preventDefault();
          }
        },
        { mozSystemGroup: true }
      );

      this.addEventListener(
        "drop",
        event => {
          if (
            lazy.contentAnalysis.isActive &&
            lazyPrefs._contentAnalysisDragDropEnabled
          ) {
            let dragService = Cc[
              "@mozilla.org/widget/dragservice;1"
            ].getService(Ci.nsIDragService);
            let dragSession = dragService.getCurrentSession(window);
            if (!dragSession) {
              return;
            }

            // Don't check items from drags that take place inside of a single
            // frame, or in a same-origin iframe hierarchy.  Drag sessions with an
            // external source have no sourceWindowContext and must be checked.
            let sourceWC = dragSession.sourceWindowContext;
            let targetWC = this.browsingContext?.currentWindowContext;
            if (!targetWC) {
              return;
            }
            if (
              sourceWC &&
              sourceWC.browsingContext.top == targetWC.browsingContext.top &&
              targetWC.documentPrincipal?.subsumes(sourceWC.documentPrincipal)
            ) {
              return;
            }

            // Create a request for each text and file item in the DataTransfer
            try {
              // Tell browser to record the event target and to delay EndDragSession
              // until the content analysis results are given.
              dragSession.sendStoreDropTargetAndDelayEndDragSession(event);

              // Submit a content analysis request for each checkable entry in the
              // DataTransfer and stop dispatching this drop event.  Reissue the
              // drop if all requests are permitted.
              let caPromises = [];
              let items = event.dataTransfer.items;
              for (let elt of items) {
                const kTextMimeTypes = [
                  "text/plain",
                  "text/html",
                  "application/x-moz-nativehtml",
                ];

                let requestFields;
                if (
                  elt.kind === "string" &&
                  kTextMimeTypes.includes(elt.type)
                ) {
                  let str = event.dataTransfer.getData(elt.type);
                  if (!str) {
                    continue;
                  }
                  requestFields = {
                    analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
                    operationTypeForDisplay:
                      Ci.nsIContentAnalysisRequest.eDroppedText,
                    textContent: str,
                  };
                } else if (elt.kind === "file") {
                  let file = elt.getAsFile();
                  requestFields = {
                    analysisType: Ci.nsIContentAnalysisRequest.eFileAttached,
                    operationTypeForDisplay:
                      Ci.nsIContentAnalysisRequest.eCustomDisplayString,
                    operationDisplayString: file.name,
                    filePath: file.mozFullPath,
                  };
                } else {
                  // Unrecognized data type -- don't send to content analysis
                  continue;
                }

                caPromises.push(
                  lazy.contentAnalysis.analyzeContentRequest(
                    {
                      reason: Ci.nsIContentAnalysisRequest.eDragAndDrop,
                      requestToken: Services.uuid.generateUUID().toString(),
                      resources: [],
                      url: lazy.contentAnalysis.getURIForDropEvent(event),
                      windowGlobalParent:
                        this.browsingContext.currentWindowContext,
                      ...requestFields,
                    },
                    true /* autoAcknowledge */
                  )
                );
              }

              if (!caPromises.length) {
                // Nothing was analyzable.
                dragSession.sendDispatchToDropTargetAndResumeEndDragSession(
                  true
                );
                return;
              }

              // Only permit the drop if all requests were approved.  Issue dragexit
              // instead of drop if CA rejected the content or there was an error.
              Promise.all(caPromises).then(
                caResults => {
                  let allApproved = caResults.reduce((prev, current) => {
                    return prev && current.shouldAllowContent;
                  }, true);
                  dragSession.sendDispatchToDropTargetAndResumeEndDragSession(
                    allApproved
                  );
                },
                () => {
                  dragSession.sendDispatchToDropTargetAndResumeEndDragSession(
                    false
                  );
                }
              );

              // Do not allow this drop to continue dispatch.
              event.preventDefault();
              event.stopPropagation();
            } catch {
              // On internal error, deny any drop.  CA has its own behavior to
              // handle internal errors, like a lost connection to the agent, but
              // we are more strict when facing errors here.
              event.preventDefault();
              event.stopPropagation();
            }
          }

          // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process
          // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
          if (
            !this.droppedLinkHandler ||
            event.defaultPrevented ||
            this.isRemoteBrowser
          ) {
            return;
          }

          let linkHandler = Services.droppedLinkHandler;
          try {
            if (!linkHandler.canDropLink(event, false)) {
              return;
            }

            // Pass true to prevent the dropping of javascript:/data: URIs
            var links = linkHandler.dropLinks(event, true);
          } catch (ex) {
            return;
          }

          if (links.length) {
            let triggeringPrincipal = linkHandler.getTriggeringPrincipal(event);
            this.droppedLinkHandler(event, links, triggeringPrincipal);
          }
        },
        { mozSystemGroup: true }
      );

      this.addEventListener("dragstart", event => {
        // If we're a remote browser dealing with a dragstart, stop it
        // from propagating up, since our content process should be dealing
        // with the mouse movement.
        if (this.isRemoteBrowser) {
          event.stopPropagation();
        }
      });
    }

    resetFields() {
      if (this.observer) {
        try {
          Services.obs.removeObserver(
            this.observer,
            "browser:purge-session-history"
          );
        } catch (ex) {
          // It's not clear why this sometimes throws an exception.
        }
        this.observer = null;
      }

      let browser = this;
      this.observer = {
        observe(aSubject, aTopic, aState) {
          if (aTopic == "browser:purge-session-history") {
            browser.purgeSessionHistory();
          } else if (aTopic == "apz:cancel-autoscroll") {
            if (aState == browser._autoScrollScrollId) {
              // Set this._autoScrollScrollId to null, so in stopScroll() we
              // don't call stopApzAutoscroll() (since it's APZ that
              // initiated the stopping).
              browser._autoScrollScrollId = null;
              browser._autoScrollPresShellId = null;

              browser._autoScrollPopup.hidePopup();
            }
          }
        },
        QueryInterface: ChromeUtils.generateQI([
          "nsIObserver",
          "nsISupportsWeakReference",
        ]),
      };

      this._documentURI = null;

      this._originalURI = null;

      this._currentAuthPromptURI = null;

      this._searchTerms = "";

      this._documentContentType = null;

      this._loadContext = null;

      this._webBrowserFind = null;

      this._finder = null;

      this._remoteFinder = null;

      this._fastFind = null;

      this._lastSearchString = null;

      this._characterSet = "";

      this._mayEnableCharacterEncodingMenu = null;

      this._contentPrincipal = null;

      this._contentPartitionedPrincipal = null;

      this._csp = null;

      this._referrerInfo = null;

      this._contentRequestContextID = null;

      this._rdmFullZoom = 1.0;

      this._isSyntheticDocument = false;

      this.mPrefs = Services.prefs;

      this._audioMuted = false;

      this._hasAnyPlayingMediaBeenBlocked = false;

      this._unselectedTabHoverMessageListenerCount = 0;

      this.urlbarChangeTracker = {
        _startedLoadSinceLastUserTyping: false,

        startedLoad() {
          this._startedLoadSinceLastUserTyping = true;
        },
        finishedLoad() {
          this._startedLoadSinceLastUserTyping = false;
        },
        userTyped() {
          this._startedLoadSinceLastUserTyping = false;
        },
      };

      this._userTypedValue = null;

      this._AUTOSCROLL_SNAP = 10;

      this._autoScrollBrowsingContext = null;

      this._startX = null;

      this._startY = null;

      this._autoScrollPopup = null;

      /**
       * These IDs identify the scroll frame being autoscrolled.
       */
      this._autoScrollScrollId = null;

      this._autoScrollPresShellId = null;
    }

    connectedCallback() {
      // We typically use this to avoid running JS that triggers a layout during parse
      // (see comment on the delayConnectedCallback implementation). In this case, we
      // are using it to avoid a leak - see https://bugzilla.mozilla.org/show_bug.cgi?id=1441935#c20.
      if (this.delayConnectedCallback()) {
        return;
      }

      this.construct();
    }

    disconnectedCallback() {
      this.destroy();
    }

    get autoscrollEnabled() {
      if (this.getAttribute("autoscroll") == "false") {
        return false;
      }

      return this.mPrefs.getBoolPref("general.autoScroll", true);
    }

    get canGoBack() {
      return this.webNavigation.canGoBack;
    }

    get canGoBackIgnoringUserInteraction() {
      return this.webNavigation.canGoBackIgnoringUserInteraction;
    }

    get canGoForward() {
      return this.webNavigation.canGoForward;
    }

    // While an auth prompt from a base domain different than the current sites is open, we want to display the url of the cross domain site.
    // This is to prevent possible auth spoofing scenarios.
    // The URL of the requesting origin is provided by 'currentAuthPromptURI', this will only be non null while an auth prompt is open.
    // See bug 791594 for reference.
    get currentURI() {
      if (this.currentAuthPromptURI) {
        return this.currentAuthPromptURI;
      }
      if (this.webNavigation) {
        return this.webNavigation.currentURI;
      }
      return null;
    }

    get documentURI() {
      return this.isRemoteBrowser
        ? this._documentURI
        : this.contentDocument?.documentURIObject;
    }

    get documentContentType() {
      if (this.isRemoteBrowser) {
        return this._documentContentType;
      }
      return this.contentDocument ? this.contentDocument.contentType : null;
    }

    set documentContentType(aContentType) {
      if (aContentType != null) {
        if (this.isRemoteBrowser) {
          this._documentContentType = aContentType;
        } else {
          this.contentDocument.documentContentType = aContentType;
        }
      }
    }

    get loadContext() {
      if (this._loadContext) {
        return this._loadContext;
      }

      let { frameLoader } = this;
      if (!frameLoader) {
        return null;
      }
      this._loadContext = frameLoader.loadContext;
      return this._loadContext;
    }

    get autoCompletePopup() {
      return document.getElementById(this.getAttribute("autocompletepopup"));
    }

    set suspendMediaWhenInactive(val) {
      this.browsingContext.suspendMediaWhenInactive = val;
    }

    get suspendMediaWhenInactive() {
      return !!this.browsingContext?.suspendMediaWhenInactive;
    }

    set docShellIsActive(val) {
      if (!this.browsingContext) {
        return;
      }
      this.browsingContext.isActive = val;
      if (this.isRemoteBrowser) {
        let remoteTab = this.frameLoader?.remoteTab;
        if (remoteTab) {
          remoteTab.renderLayers = val;
        }
      }
    }

    get docShellIsActive() {
      return !!this.browsingContext?.isActive;
    }

    set renderLayers(val) {
      if (this.isRemoteBrowser) {
        let remoteTab = this.frameLoader?.remoteTab;
        if (remoteTab) {
          remoteTab.renderLayers = val;
        }
      } else {
        this.docShellIsActive = val;
      }
    }

    get renderLayers() {
      if (this.isRemoteBrowser) {
        return !!this.frameLoader?.remoteTab?.renderLayers;
      }
      return this.docShellIsActive;
    }

    get hasLayers() {
      if (this.isRemoteBrowser) {
        return !!this.frameLoader?.remoteTab?.hasLayers;
      }
      return this.docShellIsActive;
    }

    get isRemoteBrowser() {
      return this.getAttribute("remote") == "true";
    }

    get remoteType() {
      return this.browsingContext?.currentRemoteType;
    }

    get isCrashed() {
      if (!this.isRemoteBrowser || !this.frameLoader) {
        return false;
      }

      return !this.frameLoader.remoteTab;
    }

    get messageManager() {
      // Bug 1524084 - Trying to get at the message manager while in the crashed state will
      // create a new message manager that won't shut down properly when the crashed browser
      // is removed from the DOM. We work around that right now by returning null if we're
      // in the crashed state.
      if (this.frameLoader && !this.isCrashed) {
        return this.frameLoader.messageManager;
      }
      return null;
    }

    get webBrowserFind() {
      if (!this._webBrowserFind) {
        this._webBrowserFind = this.docShell
          .QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIWebBrowserFind);
      }
      return this._webBrowserFind;
    }

    get finder() {
      if (this.isRemoteBrowser) {
        if (!this._remoteFinder) {
          this._remoteFinder = new lazy.FinderParent(this);
        }
        return this._remoteFinder;
      }
      if (!this._finder) {
        if (!this.docShell) {
          return null;
        }

        this._finder = new lazy.Finder(this.docShell);
      }
      return this._finder;
    }

    get fastFind() {
      if (!this._fastFind) {
        if (!("@mozilla.org/typeaheadfind;1" in Cc)) {
          return null;
        }

        var tabBrowser = this.getTabBrowser();
        if (tabBrowser && "fastFind" in tabBrowser) {
          return (this._fastFind = tabBrowser.fastFind);
        }

        if (!this.docShell) {
          return null;
        }

        this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(
          Ci.nsITypeAheadFind
        );
        this._fastFind.init(this.docShell);
      }
      return this._fastFind;
    }

    get outerWindowID() {
      return this.browsingContext?.currentWindowGlobal?.outerWindowId;
    }

    get innerWindowID() {
      return this.browsingContext?.currentWindowGlobal?.innerWindowId || null;
    }

    get browsingContext() {
      if (this.frameLoader) {
        return this.frameLoader.browsingContext;
      }
      return null;
    }
    /**
     * Note that this overrides webNavigation on XULFrameElement, and duplicates the return value for the non-remote case
     */
    get webNavigation() {
      return this.isRemoteBrowser
        ? this._remoteWebNavigation
        : this.docShell && this.docShell.QueryInterface(Ci.nsIWebNavigation);
    }

    get webProgress() {
      return this.browsingContext?.webProgress;
    }

    get sessionHistory() {
      return this.webNavigation.sessionHistory;
    }

    get contentTitle() {
      return (
        (this.isRemoteBrowser
          ? this.browsingContext?.currentWindowGlobal?.documentTitle
          : this.contentDocument.title) ?? ""
      );
    }

    forceEncodingDetection() {
      if (this.isRemoteBrowser) {
        this.sendMessageToActor("ForceEncodingDetection", {}, "BrowserTab");
      } else {
        this.docShell.forceEncodingDetection();
      }
    }

    get characterSet() {
      return this.isRemoteBrowser ? this._characterSet : this.docShell.charset;
    }

    get mayEnableCharacterEncodingMenu() {
      return this.isRemoteBrowser
        ? this._mayEnableCharacterEncodingMenu
        : this.docShell.mayEnableCharacterEncodingMenu;
    }

    set mayEnableCharacterEncodingMenu(aMayEnable) {
      if (this.isRemoteBrowser) {
        this._mayEnableCharacterEncodingMenu = aMayEnable;
      }
    }

    get contentPrincipal() {
      return this.isRemoteBrowser
        ? this._contentPrincipal
        : this.contentDocument.nodePrincipal;
    }

    get contentPartitionedPrincipal() {
      return this.isRemoteBrowser
        ? this._contentPartitionedPrincipal
        : this.contentDocument.partitionedPrincipal;
    }

    get cookieJarSettings() {
      return this.isRemoteBrowser
        ? this.browsingContext?.currentWindowGlobal?.cookieJarSettings
        : this.contentDocument.cookieJarSettings;
    }

    get csp() {
      return this.isRemoteBrowser ? this._csp : this.contentDocument.csp;
    }

    get contentRequestContextID() {
      if (this.isRemoteBrowser) {
        return this._contentRequestContextID;
      }
      try {
        return this.contentDocument.documentLoadGroup.requestContextID;
      } catch (e) {
        return null;
      }
    }

    get referrerInfo() {
      return this.isRemoteBrowser
        ? this._referrerInfo
        : this.contentDocument.referrerInfo;
    }

    set fullZoom(val) {
      if (val.toFixed(2) == this.fullZoom.toFixed(2)) {
        return;
      }
      if (this.browsingContext.inRDMPane) {
        this._rdmFullZoom = val;
        let event = document.createEvent("Events");
        event.initEvent("FullZoomChange", true, false);
        this.dispatchEvent(event);
      } else {
        this.browsingContext.fullZoom = val;
      }
    }

    get fullZoom() {
      if (this.browsingContext.inRDMPane) {
        return this._rdmFullZoom;
      }
      return this.browsingContext.fullZoom;
    }

    set textZoom(val) {
      if (val.toFixed(2) == this.textZoom.toFixed(2)) {
        return;
      }
      this.browsingContext.textZoom = val;
    }

    get textZoom() {
      return this.browsingContext.textZoom;
    }

    enterResponsiveMode() {
      if (this.browsingContext.inRDMPane) {
        return;
      }
      this.browsingContext.inRDMPane = true;
      this._rdmFullZoom = this.browsingContext.fullZoom;
      this.browsingContext.fullZoom = 1.0;
    }

    leaveResponsiveMode() {
      if (!this.browsingContext.inRDMPane) {
        return;
      }
      this.browsingContext.inRDMPane = false;
      this.browsingContext.fullZoom = this._rdmFullZoom;
    }

    get isSyntheticDocument() {
      if (this.isRemoteBrowser) {
        return this._isSyntheticDocument;
      }
      return this.contentDocument.mozSyntheticDocument;
    }

    get hasContentOpener() {
      return !!this.browsingContext.opener;
    }

    get audioMuted() {
      return this._audioMuted;
    }

    get shouldHandleUnselectedTabHover() {
      return this._unselectedTabHoverMessageListenerCount > 0;
    }

    set shouldHandleUnselectedTabHover(value) {
      this._unselectedTabHoverMessageListenerCount += value ? 1 : -1;
    }

    get securityUI() {
      return this.browsingContext.secureBrowserUI;
    }

    set userTypedValue(val) {
      this.urlbarChangeTracker.userTyped();
      this._userTypedValue = val;
    }

    get userTypedValue() {
      return this._userTypedValue;
    }

    get dontPromptAndDontUnload() {
      return 1;
    }

    get dontPromptAndUnload() {
      return 2;
    }

    set originalURI(aURI) {
      if (aURI instanceof Ci.nsIURI) {
        this._originalURI = aURI;
      }
    }

    get originalURI() {
      return this._originalURI;
    }

    set searchTerms(val) {
      this._searchTerms = val;
    }

    get searchTerms() {
      return this._searchTerms;
    }

    set currentAuthPromptURI(aURI) {
      this._currentAuthPromptURI = aURI;
    }

    get currentAuthPromptURI() {
      return this._currentAuthPromptURI;
    }
    _wrapURIChangeCall(fn) {
      if (!this.isRemoteBrowser) {
        this.isNavigating = true;
        try {
          fn();
        } finally {
          this.isNavigating = false;
        }
      } else {
        fn();
      }
    }

    goBack(
      requireUserInteraction = lazy.BrowserUtils
        .navigationRequireUserInteraction
    ) {
      var webNavigation = this.webNavigation;
      if (
        requireUserInteraction
          ? webNavigation.canGoBack
          : webNavigation.canGoBackIgnoringUserInteraction
      ) {
        this._wrapURIChangeCall(() =>
          webNavigation.goBack(requireUserInteraction)
        );
      }
    }

    goForward(
      requireUserInteraction = lazy.BrowserUtils
        .navigationRequireUserInteraction
    ) {
      var webNavigation = this.webNavigation;
      if (webNavigation.canGoForward) {
        this._wrapURIChangeCall(() =>
          webNavigation.goForward(requireUserInteraction)
        );
      }
    }

    reload() {
      const nsIWebNavigation = Ci.nsIWebNavigation;
      const flags = nsIWebNavigation.LOAD_FLAGS_NONE;
      this.reloadWithFlags(flags);
    }

    reloadWithFlags(aFlags) {
      this.webNavigation.reload(aFlags);
    }

    stop() {
      const nsIWebNavigation = Ci.nsIWebNavigation;
      const flags = nsIWebNavigation.STOP_ALL;
      this.webNavigation.stop(flags);
    }

    _fixLoadParamsToLoadURIOptions(params) {
      let loadFlags =
        params.loadFlags || params.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
      delete params.flags;
      params.loadFlags = loadFlags;
    }

    /**
     * throws exception for unknown schemes
     */
    loadURI(uri, params = {}) {
      if (!uri) {
        uri = lazy.blankURI;
      }
      this._fixLoadParamsToLoadURIOptions(params);
      this._wrapURIChangeCall(() => this.webNavigation.loadURI(uri, params));
    }

    /**
     * throws exception for unknown schemes
     */
    fixupAndLoadURIString(uriString, params = {}) {
      if (!uriString) {
        this.loadURI(null, params);
        return;
      }
      this._fixLoadParamsToLoadURIOptions(params);
      this._wrapURIChangeCall(() =>
        this.webNavigation.fixupAndLoadURIString(uriString, params)
      );
    }

    gotoIndex(aIndex) {
      this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex));
    }

    preserveLayers(preserve) {
      if (!this.isRemoteBrowser) {
        return;
      }
      let { frameLoader } = this;
      if (frameLoader.remoteTab) {
        frameLoader.remoteTab.preserveLayers(preserve);
      }
    }

    deprioritize() {
      if (!this.isRemoteBrowser) {
        return;
      }
      let { remoteTab } = this.frameLoader;
      if (remoteTab) {
        remoteTab.priorityHint = false;
        remoteTab.deprioritize();
      }
    }

    getTabBrowser() {
      if (this?.ownerGlobal?.gBrowser?.getTabForBrowser(this)) {
        return this.ownerGlobal.gBrowser;
      }
      return null;
    }

    addProgressListener(aListener, aNotifyMask) {
      if (!aNotifyMask) {
        aNotifyMask = Ci.nsIWebProgress.NOTIFY_ALL;
      }

      this.webProgress.addProgressListener(aListener, aNotifyMask);
    }

    removeProgressListener(aListener) {
      this.webProgress.removeProgressListener(aListener);
    }

    onPageHide() {
      // If we're browsing from the tab crashed UI to a URI that keeps
      // this browser non-remote, we'll handle that here.
      lazy.SessionStore?.maybeExitCrashedState(this);

      if (!this.docShell || !this.fastFind) {
        return;
      }
      var tabBrowser = this.getTabBrowser();
      if (
        !tabBrowser ||
        !("fastFind" in tabBrowser) ||
        tabBrowser.selectedBrowser == this
      ) {
        this.fastFind.setDocShell(this.docShell);
      }
    }

    audioPlaybackStarted() {
      if (this._audioMuted) {
        return;
      }
      let event = document.createEvent("Events");
      event.initEvent("DOMAudioPlaybackStarted", true, false);
      this.dispatchEvent(event);
    }

    audioPlaybackStopped() {
      let event = document.createEvent("Events");
      event.initEvent("DOMAudioPlaybackStopped", true, false);
      this.dispatchEvent(event);
    }

    /**
     * When the pref "media.block-autoplay-until-in-foreground" is on,
     * Gecko delays starting playback of media resources in tabs until the
     * tab has been in the foreground or resumed by tab's play tab icon.
     * - When Gecko delays starting playback of a media resource in a window,
     * it sends a message to call activeMediaBlockStarted(). This causes the
     * tab audio indicator to show.
     * - When a tab is foregrounded, Gecko starts playing all delayed media
     * resources in that tab, and sends a message to call
     * activeMediaBlockStopped(). This causes the tab audio indicator to hide.
     */
    activeMediaBlockStarted() {
      this._hasAnyPlayingMediaBeenBlocked = true;
      let event = document.createEvent("Events");
      event.initEvent("DOMAudioPlaybackBlockStarted", true, false);
      this.dispatchEvent(event);
    }

    activeMediaBlockStopped() {
      if (!this._hasAnyPlayingMediaBeenBlocked) {
        return;
      }
      this._hasAnyPlayingMediaBeenBlocked = false;
      let event = document.createEvent("Events");
      event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
      this.dispatchEvent(event);
    }

    mute(transientState) {
      if (!transientState) {
        this._audioMuted = true;
      }
      let context = this.frameLoader.browsingContext;
      context.notifyMediaMutedChanged(true);
    }

    unmute() {
      this._audioMuted = false;
      let context = this.frameLoader.browsingContext;
      context.notifyMediaMutedChanged(false);
    }

    resumeMedia() {
      this.frameLoader.browsingContext.notifyStartDelayedAutoplayMedia();
      if (this._hasAnyPlayingMediaBeenBlocked) {
        this._hasAnyPlayingMediaBeenBlocked = false;
        let event = document.createEvent("Events");
        event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
        this.dispatchEvent(event);
      }
    }

    unselectedTabHover(hovered) {
      if (!this.shouldHandleUnselectedTabHover) {
        return;
      }
      this.sendMessageToActor(
        "Browser:UnselectedTabHover",
        {
          hovered,
        },
        "UnselectedTabHover",
        "roots"
      );
    }

    didStartLoadSinceLastUserTyping() {
      return (
        !this.isNavigating &&
        this.urlbarChangeTracker._startedLoadSinceLastUserTyping
      );
    }

    constrainPopup(popup) {
      if (this.getAttribute("constrainpopups") != "false") {
        let constraintRect = this.getBoundingClientRect();
        constraintRect = new DOMRect(
          constraintRect.left + window.mozInnerScreenX,
          constraintRect.top + window.mozInnerScreenY,
          constraintRect.width,
          constraintRect.height
        );
        popup.setConstraintRect(constraintRect);
      } else {
        popup.setConstraintRect(new DOMRect(0, 0, 0, 0));
      }
    }

    construct() {
      elementsToDestroyOnUnload.add(this);
      this.resetFields();
      this.mInitialized = true;
      if (this.isRemoteBrowser) {
        /*
         * Don't try to send messages from this function. The message manager for
         * the <browser> element may not be initialized yet.
         */

        this._remoteWebNavigation = new lazy.RemoteWebNavigation(this);

        // Initialize contentPrincipal to the about:blank principal for this loadcontext
        let aboutBlank = Services.io.newURI("about:blank");
        let ssm = Services.scriptSecurityManager;
        this._contentPrincipal = ssm.getLoadContextContentPrincipal(
          aboutBlank,
          this.loadContext
        );
        this._contentPartitionedPrincipal = this._contentPrincipal;
        // CSP for about:blank is null; if we ever change _contentPrincipal above,
        // we should re-evaluate the CSP here.
        this._csp = null;

        if (!this.hasAttribute("disablehistory")) {
          Services.obs.addObserver(
            this.observer,
            "browser:purge-session-history",
            true
          );
        }
      }

      try {
        // |webNavigation.sessionHistory| will have been set by the frame
        // loader when creating the docShell as long as this xul:browser
        // doesn't have the 'disablehistory' attribute set.
        if (this.docShell && this.webNavigation.sessionHistory) {
          Services.obs.addObserver(
            this.observer,
            "browser:purge-session-history",
            true
          );

          // enable global history if we weren't told otherwise
          if (
            !this.hasAttribute("disableglobalhistory") &&
            !this.isRemoteBrowser
          ) {
            try {
              this.docShell.browsingContext.useGlobalHistory = true;
            } catch (ex) {
              // This can occur if the Places database is locked
              console.error("Error enabling browser global history: ", ex);
            }
          }
        }
      } catch (e) {
        console.error(e);
      }
      try {
        // Ensures the securityUI is initialized.
        var securityUI = this.securityUI; // eslint-disable-line no-unused-vars
      } catch (e) {}

      if (!this.isRemoteBrowser) {
        this._remoteWebNavigation = null;
        this.addEventListener("pagehide", this.onPageHide, true);
      }
    }

    /**
     * This is necessary because custom elements don't have a "real" destructor.
     * This method is called explicitly by tabbrowser, when changing remoteness,
     * and when we're disconnected or the window unloads.
     */
    destroy() {
      elementsToDestroyOnUnload.delete(this);

      // If we're browsing from the tab crashed UI to a URI that causes the tab
      // to go remote again, we catch this here, because swapping out the
      // non-remote browser for a remote one doesn't cause the pagehide event
      // to be fired. Previously, we used to do this in the frame script's
      // unload handler.
      lazy.SessionStore?.maybeExitCrashedState(this);

      // Make sure that any open select is closed.
      let menulist = document.getElementById("ContentSelectDropdown");
      if (menulist?.open) {
        lazy.SelectParentHelper.hide(menulist, this);
      }

      this.resetFields();

      if (!this.mInitialized) {
        return;
      }

      this.mInitialized = false;
      this.lastURI = null;

      if (!this.isRemoteBrowser) {
        this.removeEventListener("pagehide", this.onPageHide, true);
      }
    }

    updateForStateChange(aCharset, aDocumentURI, aContentType) {
      if (this.isRemoteBrowser && this.messageManager) {
        if (aCharset != null) {
          this._characterSet = aCharset;
        }

        if (aDocumentURI != null) {
          this._documentURI = aDocumentURI;
        }

        if (aContentType != null) {
          this._documentContentType = aContentType;
        }
      }
    }

    updateWebNavigationForLocationChange(
      aCanGoBack,
      aCanGoBackIgnoringUserInteraction,
      aCanGoForward
    ) {
      if (
        this.isRemoteBrowser &&
        this.messageManager &&
        !Services.appinfo.sessionHistoryInParent
      ) {
        this._remoteWebNavigation._canGoBack = aCanGoBack;
        this._remoteWebNavigation._canGoBackIgnoringUserInteraction =
          aCanGoBackIgnoringUserInteraction;
        this._remoteWebNavigation._canGoForward = aCanGoForward;
      }
    }

    updateForLocationChange(
      aLocation,
      aCharset,
      aMayEnableCharacterEncodingMenu,
      aDocumentURI,
      aTitle,
      aContentPrincipal,
      aContentPartitionedPrincipal,
      aCSP,
      aReferrerInfo,
      aIsSynthetic,
      aHaveRequestContextID,
      aRequestContextID,
      aContentType
    ) {
      if (this.isRemoteBrowser && this.messageManager) {
        if (aCharset != null) {
          this._characterSet = aCharset;
          this._mayEnableCharacterEncodingMenu =
            aMayEnableCharacterEncodingMenu;
        }

        if (aContentType != null) {
          this._documentContentType = aContentType;
        }

        this._remoteWebNavigation._currentURI = aLocation;
        this._documentURI = aDocumentURI;
        this._contentPrincipal = aContentPrincipal;
        this._contentPartitionedPrincipal = aContentPartitionedPrincipal;
        this._csp = aCSP;
        this._referrerInfo = aReferrerInfo;
        this._isSyntheticDocument = aIsSynthetic;
        this._contentRequestContextID = aHaveRequestContextID
          ? aRequestContextID
          : null;
      }
    }

    purgeSessionHistory() {
      if (this.isRemoteBrowser && !Services.appinfo.sessionHistoryInParent) {
        this._remoteWebNavigation._canGoBack = false;
        this._remoteWebNavigation._canGoBackIgnoringUserInteraction = false;
        this._remoteWebNavigation._canGoForward = false;
      }

      try {
        if (Services.appinfo.sessionHistoryInParent) {
          let sessionHistory = this.browsingContext?.sessionHistory;
          if (!sessionHistory) {
            return;
          }

          // place the entry at current index at the end of the history list, so it won't get removed
          if (sessionHistory.index < sessionHistory.count - 1) {
            let indexEntry = sessionHistory.getEntryAtIndex(
              sessionHistory.index
            );
            sessionHistory.addEntry(indexEntry, true);
          }

          let purge = sessionHistory.count;
          if (
            this.browsingContext.currentWindowGlobal.documentURI !=
            "about:blank"
          ) {
            --purge; // Don't remove the page the user's staring at from shistory
          }

          if (purge > 0) {
            sessionHistory.purgeHistory(purge);
          }

          return;
        }

        this.sendMessageToActor(
          "Browser:PurgeSessionHistory",
          {},
          "PurgeSessionHistory",
          "roots"
        );
      } catch (ex) {
        // This can throw if the browser has started to go away.
        if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
          throw ex;
        }
      }
    }

    createAboutBlankDocumentViewer(aPrincipal, aPartitionedPrincipal) {
      let principal = lazy.BrowserUtils.principalWithMatchingOA(
        aPrincipal,
        this.contentPrincipal
      );
      let partitionedPrincipal = lazy.BrowserUtils.principalWithMatchingOA(
        aPartitionedPrincipal,
        this.contentPartitionedPrincipal
      );

      if (this.isRemoteBrowser) {
        this.frameLoader.remoteTab.createAboutBlankDocumentViewer(
          principal,
          partitionedPrincipal
        );
      } else {
        this.docShell.createAboutBlankDocumentViewer(
          principal,
          partitionedPrincipal
        );
      }
    }

    _acquireAutoScrollWakeLock() {
      const pm = Cc["@mozilla.org/power/powermanagerservice;1"].getService(
        Ci.nsIPowerManagerService
      );
      this._autoScrollWakelock = pm.newWakeLock("autoscroll", window);
    }

    _releaseAutoScrollWakeLock() {
      if (this._autoScrollWakelock) {
        try {
          this._autoScrollWakelock.unlock();
        } catch (e) {
          // Ignore error since wake lock is already unlocked
        }
        this._autoScrollWakelock = null;
      }
    }

    stopScroll() {
      if (this._autoScrollBrowsingContext) {
        window.removeEventListener("mousemove", this, true);
        window.removeEventListener("mousedown", this, true);
        window.removeEventListener("mouseup", this, true);
        window.removeEventListener("DOMMouseScroll", this, true);
        window.removeEventListener("contextmenu", this, true);
        window.removeEventListener("keydown", this, true);
        window.removeEventListener("keypress", this, true);
        window.removeEventListener("keyup", this, true);

        let autoScrollWnd = this._autoScrollBrowsingContext.currentWindowGlobal;
        if (autoScrollWnd) {
          autoScrollWnd
            .getActor("AutoScroll")
            .sendAsyncMessage("Autoscroll:Stop", {});
        }

        try {
          Services.obs.removeObserver(this.observer, "apz:cancel-autoscroll");
        } catch (ex) {
          // It's not clear why this sometimes throws an exception
        }

        if (this._autoScrollScrollId != null) {
          this._autoScrollBrowsingContext.stopApzAutoscroll(
            this._autoScrollScrollId,
            this._autoScrollPresShellId
          );

          this._autoScrollScrollId = null;
          this._autoScrollPresShellId = null;
        }

        this._autoScrollBrowsingContext = null;
        this._releaseAutoScrollWakeLock();
      }
    }

    _getAndMaybeCreateAutoScrollPopup() {
      let autoscrollPopup = document.getElementById("autoscroller");
      if (!autoscrollPopup) {
        autoscrollPopup = document.createXULElement("panel");
        autoscrollPopup.className = "autoscroller";
        autoscrollPopup.setAttribute("consumeoutsideclicks", "true");
        autoscrollPopup.setAttribute("rolluponmousewheel", "true");
        autoscrollPopup.id = "autoscroller";
      }

      return autoscrollPopup;
    }

    startScroll({
      scrolldir,
      screenXDevPx,
      screenYDevPx,
      scrollId,
      presShellId,
      browsingContext,
    }) {
      if (!this.autoscrollEnabled) {
        return { autoscrollEnabled: false, usingApz: false };
      }

      // The popup size is 32px for the circle plus space for a 4px box-shadow
      // on each side.
      const POPUP_SIZE = 40;
      if (!this._autoScrollPopup) {
        this._autoScrollPopup = this._getAndMaybeCreateAutoScrollPopup();
        document.documentElement.appendChild(this._autoScrollPopup);
        this._autoScrollPopup.removeAttribute("hidden");
        this._autoScrollPopup.setAttribute("noautofocus", "true");
        this._autoScrollPopup.style.height = POPUP_SIZE + "px";
        this._autoScrollPopup.style.width = POPUP_SIZE + "px";
        this._autoScrollPopup.style.margin = -POPUP_SIZE / 2 + "px";
      }

      // In desktop pixels.
      let screenXDesktopPx = screenXDevPx / window.desktopToDeviceScale;
      let screenYDesktopPx = screenYDevPx / window.desktopToDeviceScale;

      let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
        Ci.nsIScreenManager
      );
      let screen = screenManager.screenForRect(
        screenXDesktopPx,
        screenYDesktopPx,
        1,
        1
      );

      // we need these attributes so themers don't need to create per-platform packages
      if (screen.colorDepth > 8) {
        // need high color for transparency
        // Exclude second-rate platforms
        this._autoScrollPopup.setAttribute(
          "transparent",
          !/BeOS|OS\/2/.test(navigator.appVersion)
        );
        // Enable translucency on Windows and Mac
        this._autoScrollPopup.setAttribute(
          "translucent",
          AppConstants.platform == "win" || AppConstants.platform == "macosx"
        );
      }

      this._autoScrollPopup.setAttribute("scrolldir", scrolldir);
      this._autoScrollPopup.addEventListener("popuphidden", this, true);

      // In CSS pixels
      let popupX;
      let popupY;
      {
        let cssToDesktopScale =
          window.devicePixelRatio / window.desktopToDeviceScale;

        // Sanitize screenX/screenY for available screen size with half the size
        // of the popup removed. The popup uses negative margins to center on the
        // coordinates we pass. Use desktop pixels to deal correctly with
        // multi-monitor / multi-dpi scenarios.
        let left = {},
          top = {},
          width = {},
          height = {};
        screen.GetAvailRectDisplayPix(left, top, width, height);

        let popupSizeDesktopPx = POPUP_SIZE * cssToDesktopScale;
        let minX = left.value + 0.5 * popupSizeDesktopPx;
        let maxX = left.value + width.value - 0.5 * popupSizeDesktopPx;
        let minY = top.value + 0.5 * popupSizeDesktopPx;
        let maxY = top.value + height.value - 0.5 * popupSizeDesktopPx;

        popupX =
          Math.max(minX, Math.min(maxX, screenXDesktopPx)) / cssToDesktopScale;
        popupY =
          Math.max(minY, Math.min(maxY, screenYDesktopPx)) / cssToDesktopScale;
      }

      // In CSS pixels.
      let screenX = screenXDevPx / window.devicePixelRatio;
      let screenY = screenYDevPx / window.devicePixelRatio;

      this._autoScrollPopup.openPopupAtScreen(popupX, popupY);
      this._ignoreMouseEvents = true;
      this._startX = screenX;
      this._startY = screenY;
      this._autoScrollBrowsingContext = browsingContext;
      this._acquireAutoScrollWakeLock();

      window.addEventListener("mousemove", this, true);
      window.addEventListener("mousedown", this, true);
      window.addEventListener("mouseup", this, true);
      window.addEventListener("DOMMouseScroll", this, true);
      window.addEventListener("contextmenu", this, true);
      window.addEventListener("keydown", this, true);
      window.addEventListener("keypress", this, true);
      window.addEventListener("keyup", this, true);

      let usingApz = false;

      if (
        scrollId != null &&
        this.mPrefs.getBoolPref("apz.autoscroll.enabled", false)
      ) {
        // If APZ is handling the autoscroll, it may decide to cancel
        // it of its own accord, so register an observer to allow it
        // to notify us of that.
        Services.obs.addObserver(this.observer, "apz:cancel-autoscroll", true);

        usingApz = browsingContext.startApzAutoscroll(
          screenXDevPx,
          screenYDevPx,
          scrollId,
          presShellId
        );

        // Save the IDs for later
        this._autoScrollScrollId = scrollId;
        this._autoScrollPresShellId = presShellId;
      }

      return { autoscrollEnabled: true, usingApz };
    }

    cancelScroll() {
      this._autoScrollPopup.hidePopup();
    }

    handleEvent(aEvent) {
      if (this._autoScrollBrowsingContext) {
        switch (aEvent.type) {
          case "mousemove": {
            var x = aEvent.screenX - this._startX;
            var y = aEvent.screenY - this._startY;

            if (
              x > this._AUTOSCROLL_SNAP ||
              x < -this._AUTOSCROLL_SNAP ||
              y > this._AUTOSCROLL_SNAP ||
              y < -this._AUTOSCROLL_SNAP
            ) {
              this._ignoreMouseEvents = false;
            }
            break;
          }
          case "mouseup":
          case "mousedown":
            // The following mouse click/auxclick event on the autoscroller
            // shouldn't be fired in web content for compatibility with Chrome.
            aEvent.preventClickEvent();
          // fallthrough
          case "contextmenu": {
            if (!this._ignoreMouseEvents) {
              // Use a timeout to prevent the mousedown from opening the popup again.
              // Ideally, we could use preventDefault here, but contenteditable
              // and middlemouse paste don't interact well. See bug 1188536.
              setTimeout(() => this._autoScrollPopup.hidePopup(), 0);
            }
            this._ignoreMouseEvents = false;
            break;
          }
          case "DOMMouseScroll": {
            this._autoScrollPopup.hidePopup();
            aEvent.preventDefault();
            break;
          }
          case "popuphidden": {
            // TODO: When the autoscroller is closed by clicking outside of it,
            //       we need to prevent following click event for compatibility
            //       with Chrome.  However, there is no way to do that for now.
            this._autoScrollPopup.removeEventListener(
              "popuphidden",
              this,
              true
            );
            this.stopScroll();
            break;
          }
          case "keydown": {
            if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
              // the escape key will be processed by
              // nsXULPopupManager::KeyDown and the panel will be closed.
              // So, don't consume the key event here.
              break;
            }
            // don't break here. we need to eat keydown events.
          }
          // fall through
          case "keypress":
          case "keyup": {
            // All keyevents should be eaten here during autoscrolling.
            aEvent.stopPropagation();
            aEvent.preventDefault();
            break;
          }
        }
      }
    }

    closeBrowser() {
      // The request comes from a XPCOM component, we'd want to redirect
      // the request to tabbrowser.
      let tabbrowser = this.getTabBrowser();
      if (tabbrowser) {
        let tab = tabbrowser.getTabForBrowser(this);
        if (tab) {
          tabbrowser.removeTab(tab);
          return;
        }
      }

      throw new Error(
        "Closing a browser which was not attached to a tabbrowser is unsupported."
      );
    }

    swapBrowsers(aOtherBrowser) {
      // The request comes from a XPCOM component, we'd want to redirect
      // the request to tabbrowser so tabbrowser will be setup correctly,
      // and it will eventually call swapDocShells.
      let ourTabBrowser = this.getTabBrowser();
      let otherTabBrowser = aOtherBrowser.getTabBrowser();
      if (ourTabBrowser && otherTabBrowser) {
        let ourTab = ourTabBrowser.getTabForBrowser(this);
        let otherTab = otherTabBrowser.getTabForBrowser(aOtherBrowser);
        ourTabBrowser.swapBrowsers(ourTab, otherTab);
        return;
      }

      // One of us is not connected to a tabbrowser, so just swap.
      this.swapDocShells(aOtherBrowser);
    }

    swapDocShells(aOtherBrowser) {
      if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser) {
        throw new Error(
          "Can only swap docshells between browsers in the same process."
        );
      }

      // Give others a chance to swap state.
      // IMPORTANT: Since a swapDocShells call does not swap the messageManager
      //            instances attached to a browser to aOtherBrowser, others
      //            will need to add the message listeners to the new
      //            messageManager.
      //            This is not a bug in swapDocShells or the FrameLoader,
      //            merely a design decision: If message managers were swapped,
      //            so that no new listeners were needed, the new
      //            aOtherBrowser.messageManager would have listeners pointing
      //            to the JS global of the current browser, which would rather
      //            easily create leaks while swapping.
      // IMPORTANT2: When the current browser element is removed from DOM,
      //             which is quite common after a swapDocShells call, its
      //             frame loader is destroyed, and that destroys the relevant
      //             message manager, which will remove the listeners.
      let event = new CustomEvent("SwapDocShells", { detail: aOtherBrowser });
      this.dispatchEvent(event);
      event = new CustomEvent("SwapDocShells", { detail: this });
      aOtherBrowser.dispatchEvent(event);

      // We need to swap fields that are tied to our docshell or related to
      // the loaded page
      // Fields which are built as a result of notifactions (pageshow/hide,
      // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
      // because these notifications are dispatched again once the docshells
      // are swapped.
      var fieldsToSwap = ["_webBrowserFind", "_rdmFullZoom"];

      if (this.isRemoteBrowser) {
        fieldsToSwap.push(
          ...[
            "_remoteWebNavigation",
            "_remoteFinder",
            "_documentURI",
            "_documentContentType",
            "_characterSet",
            "_mayEnableCharacterEncodingMenu",
            "_contentPrincipal",
            "_contentPartitionedPrincipal",
            "_isSyntheticDocument",
            "_originalURI",
            "_userTypedValue",
          ]
        );
      }

      var ourFieldValues = {};
      var otherFieldValues = {};
      for (let field of fieldsToSwap) {
        ourFieldValues[field] = this[field];
        otherFieldValues[field] = aOtherBrowser[field];
      }

      if (window.PopupNotifications) {
        PopupNotifications._swapBrowserNotifications(aOtherBrowser, this);
      }

      try {
        this.swapFrameLoaders(aOtherBrowser);
      } catch (ex) {
        // This may not be implemented for browser elements that are not
        // attached to a BrowserDOMWindow.
      }

      for (let field of fieldsToSwap) {
        this[field] = otherFieldValues[field];
        aOtherBrowser[field] = ourFieldValues[field];
      }

      if (!this.isRemoteBrowser) {
        // Null the current nsITypeAheadFind instances so that they're
        // lazily re-created on access. We need to do this because they
        // might have attached the wrong docShell.
        this._fastFind = aOtherBrowser._fastFind = null;
      } else {
        // Rewire the remote listeners
        this._remoteWebNavigation.swapBrowser(this);
        aOtherBrowser._remoteWebNavigation.swapBrowser(aOtherBrowser);

        if (this._remoteFinder) {
          this._remoteFinder.swapBrowser(this);
        }
        if (aOtherBrowser._remoteFinder) {
          aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser);
        }
      }

      event = new CustomEvent("EndSwapDocShells", { detail: aOtherBrowser });
      this.dispatchEvent(event);
      event = new CustomEvent("EndSwapDocShells", { detail: this });
      aOtherBrowser.dispatchEvent(event);
    }

    getInPermitUnload(aCallback) {
      if (this.isRemoteBrowser) {
        let { remoteTab } = this.frameLoader;
        if (!remoteTab) {
          // If we're crashed, we're definitely not in this state anymore.
          aCallback(false);
          return;
        }

        aCallback(
          this._inPermitUnload.has(this.browsingContext.currentWindowGlobal)
        );
        return;
      }

      if (!this.docShell || !this.docShell.docViewer) {
        aCallback(false);
        return;
      }
      aCallback(this.docShell.docViewer.inPermitUnload);
    }

    async asyncPermitUnload(action) {
      let wgp = this.browsingContext.currentWindowGlobal;
      if (this._inPermitUnload.has(wgp)) {
        throw new Error("permitUnload is already running for this tab.");
      }

      this._inPermitUnload.add(wgp);
      try {
        let permitUnload = await wgp.permitUnload(
          action,
          lazyPrefs.unloadTimeoutMs
        );
        return { permitUnload };
      } finally {
        this._inPermitUnload.delete(wgp);
      }
    }

    get hasBeforeUnload() {
      function hasBeforeUnload(bc) {
        if (bc.currentWindowContext?.hasBeforeUnload) {
          return true;
        }
        return bc.children.some(hasBeforeUnload);
      }
      return hasBeforeUnload(this.browsingContext);
    }

    permitUnload(action) {
      if (this.isRemoteBrowser) {
        if (!this.hasBeforeUnload) {
          return { permitUnload: true };
        }

        // Don't bother asking if this browser is hung:
        if (
          lazy.ProcessHangMonitor?.findActiveReport(this) ||
          lazy.ProcessHangMonitor?.findPausedReport(this)
        ) {
          return { permitUnload: true };
        }

        let result;
        let success;

        this.asyncPermitUnload(action).then(
          val => {
            result = val;
            success = true;
          },
          err => {
            result = err;
            success = false;
          }
        );

        // The permitUnload() promise will, alas, not call its resolution
        // callbacks after the browser window the promise lives in has closed,
        // so we have to check for that case explicitly.
        Services.tm.spinEventLoopUntilOrQuit(
          "browser-custom-element.js:permitUnload",
          () => window.closed || success !== undefined
        );
        if (success) {
          return result;
        }
        throw result;
      }

      if (!this.docShell || !this.docShell.docViewer) {
        return { permitUnload: true };
      }
      return {
        permitUnload: this.docShell.docViewer.permitUnload(),
      };
    }

    /**
     * Gets a screenshot of this browser as an ImageBitmap.
     *
     * @param {Number} x
     *   The x coordinate of the region from the underlying document to capture
     *   as a screenshot. This is ignored if fullViewport is true.
     * @param {Number} y
     *   The y coordinate of the region from the underlying document to capture
     *   as a screenshot. This is ignored if fullViewport is true.
     * @param {Number} w
     *   The width of the region from the underlying document to capture as a
     *   screenshot. This is ignored if fullViewport is true.
     * @param {Number} h
     *   The height of the region from the underlying document to capture as a
     *   screenshot. This is ignored if fullViewport is true.
     * @param {Number} scale
     *   The scale factor for the captured screenshot. See the documentation for
     *   WindowGlobalParent.drawSnapshot for more detail.
     * @param {String} backgroundColor
     *   The default background color for the captured screenshot. See the
     *   documentation for WindowGlobalParent.drawSnapshot for more detail.
     * @param {boolean|undefined} fullViewport
     *   True if the viewport rect should be captured. If this is true, the
     *   x, y, w and h parameters are ignored. Defaults to false.
     * @returns {Promise}
     * @resolves {ImageBitmap}
     */
    async drawSnapshot(
      x,
      y,
      w,
      h,
      scale,
      backgroundColor,
      fullViewport = false
    ) {
      let rect = fullViewport ? null : new DOMRect(x, y, w, h);
      try {
        return this.browsingContext.currentWindowGlobal.drawSnapshot(
          rect,
          scale,
          backgroundColor
        );
      } catch (e) {
        return false;
      }
    }

    dropLinks(aLinks, aTriggeringPrincipal) {
      if (!this.droppedLinkHandler) {
        return false;
      }
      let links = [];
      for (let i = 0; i < aLinks.length; i += 3) {
        links.push({
          url: aLinks[i],
          name: aLinks[i + 1],
          type: aLinks[i + 2],
        });
      }
      this.droppedLinkHandler(null, links, aTriggeringPrincipal);
      return true;
    }

    getContentBlockingLog() {
      let windowGlobal = this.browsingContext.currentWindowGlobal;
      if (!windowGlobal) {
        return null;
      }
      return windowGlobal.contentBlockingLog;
    }

    getContentBlockingEvents() {
      let windowGlobal = this.browsingContext.currentWindowGlobal;
      if (!windowGlobal) {
        return 0;
      }
      return windowGlobal.contentBlockingEvents;
    }

    // Send an asynchronous message to the remote child via an actor.
    // Note: use this only for messages through an actor. For old-style
    // messages, use the message manager.
    // The value of the scope argument determines which browsing contexts
    // are sent to:
    //   'all' - send to actors associated with all descendant child frames.
    //   'roots' - send only to actors associated with process roots.
    //   undefined/'' - send only to the top-level actor and not any descendants.
    sendMessageToActor(messageName, args, actorName, scope) {
      if (!this.frameLoader) {
        return;
      }

      function sendToChildren(browsingContext, childScope) {
        let windowGlobal = browsingContext.currentWindowGlobal;
        // If 'roots' is set, only send if windowGlobal.isProcessRoot is true.
        if (
          windowGlobal &&
          (childScope != "roots" || windowGlobal.isProcessRoot)
        ) {
          windowGlobal.getActor(actorName).sendAsyncMessage(messageName, args);
        }

        // Iterate as long as scope in assigned. Note that we use the original
        // passed in scope, not childScope here.
        if (scope) {
          for (let context of browsingContext.children) {
            sendToChildren(context, scope);
          }
        }
      }

      // Pass no second argument to always send to the top-level browsing context.
      sendToChildren(this.browsingContext);
    }

    enterModalState() {
      this.sendMessageToActor("EnterModalState", {}, "BrowserElement", "roots");
    }

    leaveModalState() {
      this.sendMessageToActor(
        "LeaveModalState",
        { forceLeave: true },
        "BrowserElement",
        "roots"
      );
    }

    /**
     * Can be called for a window with or without modal state.
     * If the window is not in modal state, this is a no-op.
     */
    maybeLeaveModalState() {
      this.sendMessageToActor(
        "LeaveModalState",
        { forceLeave: false },
        "BrowserElement",
        "roots"
      );
    }

    getDevicePermissionOrigins(key) {
      if (typeof key !== "string" || key.length === 0) {
        throw new Error("Key must be non empty string.");
      }
      if (!this._devicePermissionOrigins) {
        this._devicePermissionOrigins = new Map();
      }
      let origins = this._devicePermissionOrigins.get(key);
      if (!origins) {
        origins = new Set();
        this._devicePermissionOrigins.set(key, origins);
      }
      return origins;
    }

    // This method is replaced by frontend code in order to delay performing the
    // process switch until some async operatin is completed.
    //
    // This is used by tabbrowser to flush SessionStore before a process switch.
    async prepareToChangeRemoteness() {
      /* no-op unless replaced */
    }

    // This method is replaced by frontend code in order to handle restoring
    // remote session history
    //
    // Called immediately after changing remoteness.  If this method returns
    // `true`, Gecko will assume frontend handled resuming the load, and will
    // not attempt to resume the load itself.
    afterChangeRemoteness() {
      /* no-op unless replaced */
      return false;
    }

    // Called by Gecko before the remoteness change happens, allowing for
    // listeners, etc. to be stashed before the process switch.
    beforeChangeRemoteness() {
      // Fire the `WillChangeBrowserRemoteness` event, which may be hooked by
      // frontend code for custom behaviour.
      let event = document.createEvent("Events");
      event.initEvent("WillChangeBrowserRemoteness", true, false);
      this.dispatchEvent(event);

      // Destroy ourselves to unregister from observer notifications
      // FIXME: Can we get away with something less destructive here?
      this.destroy();
    }

    finishChangeRemoteness(redirectLoadSwitchId) {
      // Re-construct ourselves after the destroy in `beforeChangeRemoteness`.
      this.construct();

      // Fire the `DidChangeBrowserRemoteness` event, which may be hooked by
      // frontend code for custom behaviour.
      let event = document.createEvent("Events");
      event.initEvent("DidChangeBrowserRemoteness", true, false);
      this.dispatchEvent(event);

      // Call into frontend code which may want to handle the load (e.g. to
      // while restoring session state).
      return this.afterChangeRemoteness(redirectLoadSwitchId);
    }
  }

  MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIBrowser]);
  customElements.define("browser", MozBrowser);
}
