tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

InfoBar.sys.mjs (23349B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* eslint-disable no-use-before-define */
      6 const lazy = {};
      7 
      8 ChromeUtils.defineESModuleGetters(lazy, {
      9  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     10  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     11  RemoteL10n: "resource:///modules/asrouter/RemoteL10n.sys.mjs",
     12  SpecialMessageActions:
     13    "resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
     14 });
     15 
     16 const TYPES = {
     17  UNIVERSAL: "universal",
     18  GLOBAL: "global",
     19 };
     20 
     21 const FTL_FILES = [
     22  "browser/newtab/asrouter.ftl",
     23  "browser/defaultBrowserNotification.ftl",
     24  "browser/profiles.ftl",
     25  "browser/termsofuse.ftl",
     26 ];
     27 
     28 class InfoBarNotification {
     29  constructor(message, dispatch) {
     30    this._dispatch = dispatch;
     31    this.dispatchUserAction = this.dispatchUserAction.bind(this);
     32    this.buttonCallback = this.buttonCallback.bind(this);
     33    this.infobarCallback = this.infobarCallback.bind(this);
     34    this.message = message;
     35    this.notification = null;
     36    const dismissPrefConfig = message?.content?.dismissOnPrefChange;
     37    // If set, these are the prefs to watch for changes to auto-dismiss the infobar.
     38    if (Array.isArray(dismissPrefConfig)) {
     39      this._dismissPrefs = dismissPrefConfig;
     40    } else if (dismissPrefConfig) {
     41      this._dismissPrefs = [dismissPrefConfig];
     42    } else {
     43      this._dismissPrefs = [];
     44    }
     45    this._prefObserver = null;
     46  }
     47 
     48  /**
     49   * Ensure a hidden container of <a data-l10n-name> templates exists, and
     50   * inject the request links using hrefs from message.content.linkUrls.
     51   */
     52  _ensureLinkTemplatesFor(doc, names) {
     53    let container = doc.getElementById("infobar-link-templates");
     54    // We inject a hidden <div> of <a data-l10n-name> templates into the
     55    // document because Fluent’s DOM-overlay scans the page for those
     56    // placeholders.
     57    if (!container) {
     58      container = doc.createElement("div");
     59      container.id = "infobar-link-templates";
     60      container.hidden = true;
     61      doc.body.appendChild(container);
     62    }
     63 
     64    const linkUrls = this.message.content.linkUrls || {};
     65    for (let name of names) {
     66      if (!container.querySelector(`a[data-l10n-name="${name}"]`)) {
     67        const a = doc.createElement("a");
     68        a.dataset.l10nName = name;
     69        a.href = linkUrls[name];
     70        container.appendChild(a);
     71      }
     72    }
     73  }
     74 
     75  /**
     76   * Async helper to render a Fluent string. If the translation contains `<a
     77   * data-l10n-name>`, it will parse and inject the associated link contained
     78   * in the message.
     79   */
     80  async _buildMessageFragment(doc, browser, stringId, args) {
     81    // Get the raw HTML translation
     82    const html = await lazy.RemoteL10n.formatLocalizableText({
     83      string_id: stringId,
     84      ...(args && { args }),
     85    });
     86 
     87    // If no inline anchors, just return a span
     88    if (!html.includes('data-l10n-name="')) {
     89      return lazy.RemoteL10n.createElement(doc, "span", {
     90        content: { string_id: stringId, ...(args && { args }) },
     91      });
     92    }
     93 
     94    // Otherwise parse it and set up a fragment
     95    const temp = new DOMParser().parseFromString(html, "text/html").body;
     96    const frag = doc.createDocumentFragment();
     97 
     98    // Prepare <a data-l10n-name> templates
     99    const names = [...temp.querySelectorAll("a[data-l10n-name]")].map(
    100      a => a.dataset.l10nName
    101    );
    102    this._ensureLinkTemplatesFor(doc, names);
    103 
    104    // Import each node and wire up any anchors it contains
    105    for (const node of temp.childNodes) {
    106      // Nodes from DOMParser belong to a different document, so importNode()
    107      // clones them into our target doc
    108      const importedNode = doc.importNode(node, true);
    109 
    110      if (importedNode.nodeType === Node.ELEMENT_NODE) {
    111        // collect this node if it's an anchor, and all child anchors
    112        const anchors = [];
    113        if (importedNode.matches("a[data-l10n-name]")) {
    114          anchors.push(importedNode);
    115        }
    116        anchors.push(...importedNode.querySelectorAll("a[data-l10n-name]"));
    117 
    118        const linkActions = this.message.content.linkActions || {};
    119 
    120        for (const a of anchors) {
    121          const name = a.dataset.l10nName;
    122          const template = doc
    123            .getElementById("infobar-link-templates")
    124            .querySelector(`a[data-l10n-name="${name}"]`);
    125          if (!template) {
    126            continue;
    127          }
    128          a.href = template.href;
    129          a.addEventListener("click", e => {
    130            e.preventDefault();
    131            // Open link URL
    132            try {
    133              lazy.SpecialMessageActions.handleAction(
    134                {
    135                  type: "OPEN_URL",
    136                  data: { args: a.href, where: args?.where || "tab" },
    137                },
    138                browser
    139              );
    140            } catch (err) {
    141              console.error(`Error handling OPEN_URL action:`, err);
    142            }
    143            // Then fire the defined actions for that link, if applicable
    144            if (linkActions[name]) {
    145              try {
    146                lazy.SpecialMessageActions.handleAction(
    147                  linkActions[name],
    148                  browser
    149                );
    150              } catch (err) {
    151                console.error(
    152                  `Error handling ${linkActions[name]} action:`,
    153                  err
    154                );
    155              }
    156              if (linkActions[name].dismiss) {
    157                this.notification?.dismiss();
    158              }
    159            }
    160          });
    161        }
    162      }
    163 
    164      frag.appendChild(importedNode);
    165    }
    166 
    167    return frag;
    168  }
    169 
    170  /**
    171   * Displays the infobar notification in the specified browser and sends an impression ping.
    172   * Formats the message and buttons, and appends the notification.
    173   * For universal infobars, only records an impression for the first instance.
    174   *
    175   * @param {object} browser - The browser reference for the currently selected tab.
    176   */
    177  async showNotification(browser) {
    178    let { content } = this.message;
    179    let { gBrowser } = browser.ownerGlobal;
    180    let doc = gBrowser.ownerDocument;
    181    let notificationContainer;
    182    if ([TYPES.GLOBAL, TYPES.UNIVERSAL].includes(content.type)) {
    183      notificationContainer = browser.ownerGlobal.gNotificationBox;
    184    } else {
    185      notificationContainer = gBrowser.getNotificationBox(browser);
    186    }
    187 
    188    let priority = content.priority || notificationContainer.PRIORITY_SYSTEM;
    189 
    190    let labelNode = await this.formatMessageConfig(doc, browser, content.text);
    191 
    192    this.notification = await notificationContainer.appendNotification(
    193      this.message.id,
    194      {
    195        label: labelNode,
    196        image: content.icon || "chrome://branding/content/icon64.png",
    197        priority,
    198        eventCallback: this.infobarCallback,
    199        style: content.style || {},
    200      },
    201      content.buttons.map(b => this.formatButtonConfig(b)),
    202      true, // Disables clickjacking protections
    203      content.dismissable
    204    );
    205    // If the infobar is universal, only record an impression for the first
    206    // instance.
    207    if (
    208      content.type !== TYPES.UNIVERSAL ||
    209      !InfoBar._universalInfobars.length
    210    ) {
    211      this.addImpression(browser);
    212    }
    213 
    214    // Only add if the universal infobar is still active. Prevents race condition
    215    // where a notification could add itself after removeUniversalInfobars().
    216    if (
    217      content.type === TYPES.UNIVERSAL &&
    218      InfoBar._activeInfobar?.message?.id === this.message.id
    219    ) {
    220      InfoBar._universalInfobars.push({
    221        box: notificationContainer,
    222        notification: this.notification,
    223      });
    224    }
    225 
    226    // After the notification exists, attach a pref observer if applicable.
    227    this._maybeAttachPrefObserver();
    228  }
    229 
    230  _createLinkNode(doc, browser, { href, where = "tab", string_id, args, raw }) {
    231    const a = doc.createElement("a");
    232    a.href = href;
    233    a.addEventListener("click", e => {
    234      e.preventDefault();
    235      lazy.SpecialMessageActions.handleAction(
    236        { type: "OPEN_URL", data: { args: a.href, where } },
    237        browser
    238      );
    239    });
    240 
    241    if (string_id) {
    242      // wrap a localized span inside
    243      const span = lazy.RemoteL10n.createElement(doc, "span", {
    244        content: { string_id, ...(args && { args }) },
    245      });
    246      a.appendChild(span);
    247    } else {
    248      a.textContent = raw || "";
    249    }
    250 
    251    return a;
    252  }
    253 
    254  async formatMessageConfig(doc, browser, content) {
    255    const frag = doc.createDocumentFragment();
    256    const parts = Array.isArray(content) ? content : [content];
    257 
    258    for (const part of parts) {
    259      if (!part) {
    260        continue;
    261      }
    262      if (part.href) {
    263        frag.appendChild(this._createLinkNode(doc, browser, part));
    264        continue;
    265      }
    266 
    267      if (part.string_id) {
    268        const subFrag = await this._buildMessageFragment(
    269          doc,
    270          browser,
    271          part.string_id,
    272          part.args
    273        );
    274        frag.appendChild(subFrag);
    275        continue;
    276      }
    277 
    278      if (typeof part === "string") {
    279        frag.appendChild(doc.createTextNode(part));
    280        continue;
    281      }
    282 
    283      if (part.raw && typeof part.raw === "string") {
    284        frag.appendChild(doc.createTextNode(part.raw));
    285      }
    286    }
    287 
    288    return frag;
    289  }
    290 
    291  formatButtonConfig(button) {
    292    let btnConfig = { callback: this.buttonCallback, ...button };
    293    // notificationbox will set correct data-l10n-id attributes if passed in
    294    // using the l10n-id key. Otherwise the `button.label` text is used.
    295    if (button.label.string_id) {
    296      btnConfig["l10n-id"] = button.label.string_id;
    297    }
    298 
    299    return btnConfig;
    300  }
    301 
    302  handleImpressionAction(browser) {
    303    const ALLOWED_IMPRESSION_ACTIONS = ["SET_PREF"];
    304    const impressionAction = this.message.content.impression_action;
    305    const actions =
    306      impressionAction.type === "MULTI_ACTION"
    307        ? impressionAction.data.actions
    308        : [impressionAction];
    309 
    310    actions.forEach(({ type, data, once }) => {
    311      if (!ALLOWED_IMPRESSION_ACTIONS.includes(type)) {
    312        return;
    313      }
    314 
    315      let { messageImpressions } = lazy.ASRouter.state;
    316      // If we only want to perform the action on first impression, ensure no
    317      // impressions exist for this message.
    318      if (once && messageImpressions[this.message.id]?.length) {
    319        return;
    320      }
    321 
    322      data.onImpression = true;
    323      try {
    324        lazy.SpecialMessageActions.handleAction({ type, data }, browser);
    325      } catch (err) {
    326        console.error(`Error handling ${type} impression action:`, err);
    327      }
    328    });
    329  }
    330 
    331  addImpression(browser) {
    332    // If the message has an impression action, handle it before dispatching the
    333    // impression. `this._dispatch` may be async and we want to ensure we have a
    334    // consistent impression count when handling impression actions that should
    335    // only occur once.
    336    if (this.message.content.impression_action) {
    337      this.handleImpressionAction(browser);
    338    }
    339    // Record an impression in ASRouter for frequency capping
    340    this._dispatch({ type: "IMPRESSION", data: this.message });
    341    // Send a user impression telemetry ping
    342    this.sendUserEventTelemetry("IMPRESSION");
    343  }
    344 
    345  /**
    346   * Callback fired when a button in the infobar is clicked.
    347   *
    348   * @param {Element} notificationBox - The `<notification-message>` element representing the infobar.
    349   * @param {object} btnDescription - An object describing the button, includes the label, the action with an optional dismiss property, and primary button styling.
    350   * @param {Element} target - The <button> DOM element that was clicked.
    351   * @returns {boolean} `true` to keep the infobar open, `false` to dismiss it.
    352   */
    353  buttonCallback(notificationBox, btnDescription, target) {
    354    this.dispatchUserAction(
    355      btnDescription.action,
    356      target.ownerGlobal.gBrowser.selectedBrowser
    357    );
    358    let isPrimary = target.classList.contains("primary");
    359    let eventName = isPrimary
    360      ? "CLICK_PRIMARY_BUTTON"
    361      : "CLICK_SECONDARY_BUTTON";
    362    this.sendUserEventTelemetry(eventName);
    363 
    364    // Prevents infobar dismissal when dismiss is explicitly set to `false`
    365    return btnDescription.action?.dismiss === false;
    366  }
    367 
    368  dispatchUserAction(action, selectedBrowser) {
    369    this._dispatch({ type: "USER_ACTION", data: action }, selectedBrowser);
    370  }
    371 
    372  /**
    373   * Handles infobar events triggered by the notification interactions (excluding button clicks).
    374   * Cleans up the notification and active infobar state when the infobar is removed or dismissed.
    375   * If the removed infobar is universal, ensures all universal infobars and related observers are also removed.
    376   *
    377   * @param {string} eventType - The type of event (e.g., "removed").
    378   */
    379  infobarCallback(eventType) {
    380    // Clean up the pref observer on any removal/dismissal path.
    381    this._removePrefObserver();
    382    const wasUniversal = this.message.content.type === TYPES.UNIVERSAL;
    383    const isActiveMessage =
    384      InfoBar._activeInfobar?.message?.id === this.message.id;
    385    if (eventType === "removed") {
    386      this.notification = null;
    387      if (isActiveMessage) {
    388        InfoBar._activeInfobar = null;
    389      }
    390    } else if (this.notification) {
    391      this.sendUserEventTelemetry("DISMISSED");
    392      this.notification = null;
    393 
    394      if (isActiveMessage) {
    395        InfoBar._activeInfobar = null;
    396      }
    397    }
    398    // If one instance of universal infobar is removed, remove all instances and
    399    // the new window observer
    400    if (wasUniversal && isActiveMessage && InfoBar._universalInfobars.length) {
    401      this.removeUniversalInfobars();
    402    }
    403  }
    404 
    405  /**
    406   * If content.dismissOnPrefChange is set (string or array), observe those
    407   * pref(s) and dismiss the infobar whenever any of them changes (including
    408   * when it is set for the first time).
    409   */
    410  _maybeAttachPrefObserver() {
    411    if (!this._dismissPrefs?.length || this._prefObserver) {
    412      return;
    413    }
    414    // Weak observer to avoid leaks.
    415    this._prefObserver = {
    416      QueryInterface: ChromeUtils.generateQI([
    417        "nsIObserver",
    418        "nsISupportsWeakReference",
    419      ]),
    420      observe: (subject, topic, data) => {
    421        if (topic === "nsPref:changed" && this._dismissPrefs.includes(data)) {
    422          try {
    423            this.notification?.dismiss();
    424          } catch (e) {
    425            console.error("Failed to dismiss infobar on pref change:", e);
    426          }
    427        }
    428      },
    429    };
    430    try {
    431      // Register each pref with a weak observer and ignore per-pref failures.
    432      for (const pref of this._dismissPrefs) {
    433        try {
    434          Services.prefs.addObserver(pref, this._prefObserver, true);
    435        } catch (_) {}
    436      }
    437    } catch (e) {
    438      console.error(
    439        "Failed to add prefs observer(s) for dismissOnPrefChange:",
    440        e
    441      );
    442    }
    443  }
    444 
    445  _removePrefObserver() {
    446    if (!this._dismissPrefs?.length || !this._prefObserver) {
    447      return;
    448    }
    449    for (const pref of this._dismissPrefs) {
    450      try {
    451        Services.prefs.removeObserver(pref, this._prefObserver);
    452      } catch (_) {
    453        // Ignore as the observer might already be removed during shutdown/teardown.
    454      }
    455    }
    456    this._prefObserver = null;
    457  }
    458 
    459  /**
    460   * Removes all active universal infobars from each window.
    461   * Unregisters the observer for new windows, clears the tracking array, and resets the
    462   * active infobar state.
    463   */
    464  removeUniversalInfobars() {
    465    // Remove the new window observer
    466    if (InfoBar._observingWindowOpened) {
    467      InfoBar._observingWindowOpened = false;
    468      Services.obs.removeObserver(InfoBar, "domwindowopened");
    469    }
    470    // Remove the universal infobar
    471    InfoBar._universalInfobars.forEach(({ box, notification }) => {
    472      try {
    473        if (box && notification) {
    474          box.removeNotification(notification);
    475        }
    476      } catch (error) {
    477        console.error("Failed to remove notification: ", error);
    478      }
    479    });
    480    InfoBar._universalInfobars = [];
    481 
    482    if (InfoBar._activeInfobar?.message.content.type === TYPES.UNIVERSAL) {
    483      InfoBar._activeInfobar = null;
    484    }
    485  }
    486 
    487  sendUserEventTelemetry(event) {
    488    const ping = {
    489      message_id: this.message.id,
    490      event,
    491    };
    492    this._dispatch({
    493      type: "INFOBAR_TELEMETRY",
    494      data: { action: "infobar_user_event", ...ping },
    495    });
    496  }
    497 }
    498 
    499 export const InfoBar = {
    500  _activeInfobar: null,
    501  _universalInfobars: [],
    502  _observingWindowOpened: false,
    503 
    504  maybeLoadCustomElement(win) {
    505    if (!win.customElements.get("remote-text")) {
    506      Services.scriptloader.loadSubScript(
    507        "chrome://browser/content/asrouter/components/remote-text.js",
    508        win
    509      );
    510    }
    511  },
    512 
    513  maybeInsertFTL(win) {
    514    FTL_FILES.forEach(path => win.MozXULElement.insertFTLIfNeeded(path));
    515  },
    516 
    517  /**
    518   * Helper to check the window's state and whether it's a
    519   * private browsing window, a popup or a taskbar tab.
    520   *
    521   * @returns {boolean} `true` if the window is valid for showing an infobar.
    522   */
    523  isValidInfobarWindow(win) {
    524    if (!win || win.closed) {
    525      return false;
    526    }
    527    if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
    528      return false;
    529    }
    530    if (!win.toolbar?.visible) {
    531      // Popups don't have a visible toolbar
    532      return false;
    533    }
    534    if (win.document.documentElement.hasAttribute("taskbartab")) {
    535      return false;
    536    }
    537    return true;
    538  },
    539 
    540  /**
    541   * Displays the universal infobar in all open, fully loaded browser windows.
    542   *
    543   * @param {InfoBarNotification} notification - The notification instance to display.
    544   */
    545  async showNotificationAllWindows(notification) {
    546    for (let win of Services.wm.getEnumerator("navigator:browser")) {
    547      if (
    548        !win.gBrowser ||
    549        win.document?.readyState !== "complete" ||
    550        !this.isValidInfobarWindow(win)
    551      ) {
    552        continue;
    553      }
    554      this.maybeLoadCustomElement(win);
    555      this.maybeInsertFTL(win);
    556      const browser = win.gBrowser.selectedBrowser;
    557      await notification.showNotification(browser);
    558    }
    559  },
    560 
    561  _maybeReplaceActiveInfoBar(nextMessage) {
    562    if (!this._activeInfobar) {
    563      return false;
    564    }
    565    const replacementEligible = nextMessage?.content?.canReplace || [];
    566    const activeId = this._activeInfobar.message?.id;
    567    if (!replacementEligible.includes(activeId)) {
    568      return false;
    569    }
    570    const activeType = this._activeInfobar.message?.content?.type;
    571    if (activeType === TYPES.UNIVERSAL) {
    572      this._activeInfobar.notification?.removeUniversalInfobars();
    573    } else {
    574      try {
    575        this._activeInfobar.notification?.notification.dismiss();
    576      } catch (e) {
    577        console.error("Failed to dismiss active infobar:", e);
    578      }
    579    }
    580    this._activeInfobar = null;
    581    return true;
    582  },
    583 
    584  /**
    585   * Displays an infobar notification in the specified browser window.
    586   * For the first universal infobar, shows the notification in all open browser windows
    587   * and sets up an observer to handle new windows.
    588   * For non-universal, displays the notification only in the given window.
    589   *
    590   * @param {object} browser - The browser reference for the currently selected tab.
    591   * @param {object} message - The message object describing the infobar content.
    592   * @param {function} dispatch - The dispatch function for actions.
    593   * @param {boolean} universalInNewWin - `True` if this is a universal infobar for a new window.
    594   * @returns {Promise<InfoBarNotification|null>} The notification instance, or null if not shown.
    595   */
    596  async showInfoBarMessage(browser, message, dispatch, universalInNewWin) {
    597    const win = browser?.ownerGlobal;
    598    if (!this.isValidInfobarWindow(win)) {
    599      return null;
    600    }
    601    const isUniversal = message.content.type === TYPES.UNIVERSAL;
    602    // Check if this is the first instance of a universal infobar
    603    const isFirstUniversal = !universalInNewWin && isUniversal;
    604    // Prevent stacking multiple infobars
    605    if (this._activeInfobar && !universalInNewWin) {
    606      // Check if infobar is configured to replace the current infobar.
    607      if (!this._maybeReplaceActiveInfoBar(message)) {
    608        return null;
    609      }
    610    }
    611 
    612    this.maybeLoadCustomElement(win);
    613    this.maybeInsertFTL(win);
    614 
    615    let notification = new InfoBarNotification(message, dispatch);
    616 
    617    if (!universalInNewWin) {
    618      this._activeInfobar = { message, dispatch, notification };
    619    }
    620 
    621    if (isFirstUniversal) {
    622      await this.showNotificationAllWindows(notification);
    623      if (!this._observingWindowOpened) {
    624        this._observingWindowOpened = true;
    625        Services.obs.addObserver(this, "domwindowopened");
    626      } else {
    627        // TODO: At least during testing it seems that we can get here more
    628        // than once without passing through removeUniversalInfobars(). Is
    629        // this expected?
    630        console.warn(
    631          "InfoBar: Already observing new windows for universal infobar."
    632        );
    633      }
    634    } else {
    635      await notification.showNotification(browser);
    636    }
    637 
    638    if (!universalInNewWin) {
    639      this._activeInfobar = { message, dispatch, notification };
    640      // If the window closes before the user interacts with the active infobar,
    641      // clear it
    642      win.addEventListener(
    643        "unload",
    644        () => {
    645          // Remove this window’s stale entry
    646          InfoBar._universalInfobars = InfoBar._universalInfobars.filter(
    647            ({ box }) => box.ownerGlobal !== win
    648          );
    649 
    650          if (isUniversal) {
    651            // If there’s still at least one live universal infobar,
    652            // make it the active infobar; otherwise clear the active infobar
    653            const nextEntry = InfoBar._universalInfobars.find(
    654              ({ box }) => !box.ownerGlobal?.closed
    655            );
    656            const nextNotification = nextEntry?.notification;
    657            InfoBar._activeInfobar = nextNotification
    658              ? { message, dispatch, nextNotification }
    659              : null;
    660          } else {
    661            // Non-universal always clears on unload
    662            InfoBar._activeInfobar = null;
    663          }
    664        },
    665        { once: true }
    666      );
    667    }
    668 
    669    return notification;
    670  },
    671 
    672  /**
    673   * Observer callback fired when a new window is opened.
    674   * If the topic is "domwindowopened" and the window is a valid target,
    675   * the universal infobar will be shown in the new window once loaded.
    676   *
    677   * @param {Window} aSubject - The newly opened window.
    678   * @param {string} aTopic - The topic of the observer notification.
    679   */
    680  observe(aSubject, aTopic) {
    681    if (aTopic !== "domwindowopened") {
    682      return;
    683    }
    684    const win = aSubject;
    685 
    686    if (!this.isValidInfobarWindow(win)) {
    687      return;
    688    }
    689 
    690    const { message, dispatch } = this._activeInfobar || {};
    691    if (!message || message.content.type !== TYPES.UNIVERSAL) {
    692      return;
    693    }
    694 
    695    const onWindowReady = () => {
    696      if (!win.gBrowser || win.closed) {
    697        return;
    698      }
    699      if (
    700        !InfoBar._activeInfobar ||
    701        InfoBar._activeInfobar.message !== message
    702      ) {
    703        return;
    704      }
    705      this.showInfoBarMessage(
    706        win.gBrowser.selectedBrowser,
    707        message,
    708        dispatch,
    709        true
    710      );
    711    };
    712 
    713    if (win.document?.readyState === "complete") {
    714      onWindowReady();
    715    } else {
    716      win.addEventListener("load", onWindowReady, { once: true });
    717    }
    718  },
    719 };