Bug 1544875 - Inline FirefoxMonitor.jsm into api.js. r=johannh
☠☠ backed out by e639999f1f28 ☠ ☠
authorNihanth Subramanya <nhnt11@gmail.com>
Thu, 18 Apr 2019 14:43:05 +0000
changeset 470471 06b93c66c6e0b3d948e1b225757596a3270d0e51
parent 470470 bb36364808d5854026b816428a81b08c5924e2ef
child 470472 515cfc853f2a8372dcc9139f8b90a4e7fd038744
push id35906
push useraciure@mozilla.com
push dateTue, 23 Apr 2019 22:14:56 +0000
treeherdermozilla-central@0ce3633f8b80 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjohannh
bugs1544875
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1544875 - Inline FirefoxMonitor.jsm into api.js. r=johannh Depends on D27797 Differential Revision: https://phabricator.services.mozilla.com/D27891
browser/extensions/fxmonitor/moz.build
browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm
browser/extensions/fxmonitor/privileged/api.js
--- a/browser/extensions/fxmonitor/moz.build
+++ b/browser/extensions/fxmonitor/moz.build
@@ -17,14 +17,13 @@ FINAL_TARGET_FILES.features['fxmonitor@m
 FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['assets'] += [
   'assets/alert.svg',
   'assets/monitor32.svg'
 ]
 
 FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged'] += [
   'privileged/api.js',
   'privileged/FirefoxMonitor.css',
-  'privileged/FirefoxMonitor.jsm',
   'privileged/schema.json'
 ]
 
 with Files('**'):
   BUG_COMPONENT = ('Firefox', 'Firefox Monitor')
deleted file mode 100644
--- a/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm
+++ /dev/null
@@ -1,525 +0,0 @@
-/* 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/. */
-
-/* globals Services, XPCOMUtils */
-
-ChromeUtils.defineModuleGetter(this, "EveryWindow",
-                               "resource:///modules/EveryWindow.jsm");
-ChromeUtils.defineModuleGetter(this, "PluralForm",
-                               "resource://gre/modules/PluralForm.jsm");
-ChromeUtils.defineModuleGetter(this, "Preferences",
-                               "resource://gre/modules/Preferences.jsm");
-ChromeUtils.defineModuleGetter(this, "RemoteSettings",
-                               "resource://services-settings/remote-settings.js");
-
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-this.FirefoxMonitor = {
-  // Map of breached site host -> breach metadata.
-  domainMap: new Map(),
-
-  // Set of hosts for which the user has already been shown,
-  // and interacted with, the popup.
-  warnedHostsSet: new Set(),
-
-  // The above set is persisted as a JSON string in this pref.
-  kWarnedHostsPref: "extensions.fxmonitor.warnedHosts",
-
-  // Reference to the extension object from the WebExtension context.
-  // Used for getting URIs for resources packaged in the extension.
-  extension: null,
-
-  // Whether we've started observing for the user visiting a breached site.
-  observerAdded: false,
-
-  // This is here for documentation, will be redefined to a lazy getter
-  // that creates and returns a string bundle in loadStrings().
-  strings: null,
-
-  // This is here for documentation, will be redefined to a pref getter
-  // using XPCOMUtils.defineLazyPreferenceGetter in init().
-  enabled: null,
-
-  kEnabledPref: "extensions.fxmonitor.enabled",
-
-  // Telemetry event recording is enabled by default.
-  // If this pref exists and is true-y, it's disabled.
-  kTelemetryDisabledPref: "extensions.fxmonitor.telemetryDisabled",
-
-  kNotificationID: "fxmonitor",
-
-  // This is here for documentation, will be redefined to a pref getter
-  // using XPCOMUtils.defineLazyPreferenceGetter in delayedInit().
-  // The value of this property is used as the URL to which the user
-  // is directed when they click "Check Firefox Monitor".
-  FirefoxMonitorURL: null,
-  kFirefoxMonitorURLPref: "extensions.fxmonitor.FirefoxMonitorURL",
-  kDefaultFirefoxMonitorURL: "https://monitor.firefox.com",
-
-  // This is here for documentation, will be redefined to a pref getter
-  // using XPCOMUtils.defineLazyPreferenceGetter in delayedInit().
-  // The pref stores whether the user has seen a breach alert already.
-  // The value is used in warnIfNeeded.
-  firstAlertShown: null,
-  kFirstAlertShownPref: "extensions.fxmonitor.firstAlertShown",
-
-  disable() {
-    Preferences.set(this.kEnabledPref, false);
-  },
-
-  getURL(aPath) {
-    return this.extension.getURL(aPath);
-  },
-
-  getString(aKey) {
-    return this.strings.GetStringFromName(aKey);
-  },
-
-  getFormattedString(aKey, args) {
-    return this.strings.formatStringFromName(aKey, args, args.length);
-  },
-
-  init(aExtension) {
-    this.extension = aExtension;
-
-    XPCOMUtils.defineLazyPreferenceGetter(
-      this, "enabled", this.kEnabledPref, true,
-      (pref, oldVal, newVal) => {
-        if (newVal) {
-          this.startObserving();
-        } else {
-          this.stopObserving();
-        }
-      }
-    );
-
-    if (this.enabled) {
-      this.startObserving();
-    }
-  },
-
-  // Used to enforce idempotency of delayedInit. delayedInit is
-  // called in startObserving() to ensure we load our strings, etc.
-  _delayedInited: false,
-  async delayedInit() {
-    if (this._delayedInited) {
-      return;
-    }
-
-    this._delayedInited = true;
-
-    // Expire our telemetry on November 1, at which time
-    // we should redo data-review.
-    let telemetryExpiryDate = new Date(2019, 10, 1); // Month is zero-index
-    let today = new Date();
-    let expired = today.getTime() > telemetryExpiryDate.getTime();
-
-    Services.telemetry.registerEvents("fxmonitor", {
-      "interaction": {
-        methods: ["interaction"],
-        objects: [
-          "doorhanger_shown",
-          "doorhanger_removed",
-          "check_btn",
-          "dismiss_btn",
-          "never_show_btn",
-        ],
-        record_on_release: true,
-        expired,
-      },
-    });
-
-    let telemetryEnabled = !Preferences.get(this.kTelemetryDisabledPref);
-    Services.telemetry.setEventRecordingEnabled("fxmonitor", telemetryEnabled);
-
-    let warnedHostsJSON = Preferences.get(this.kWarnedHostsPref, "");
-    if (warnedHostsJSON) {
-      try {
-        let json = JSON.parse(warnedHostsJSON);
-        this.warnedHostsSet = new Set(json);
-      } catch (ex) {
-        // Invalid JSON, invalidate the pref.
-        Preferences.reset(this.kWarnedHostsPref);
-      }
-    }
-
-    XPCOMUtils.defineLazyPreferenceGetter(this, "FirefoxMonitorURL",
-      this.kFirefoxMonitorURLPref, this.kDefaultFirefoxMonitorURL);
-
-    XPCOMUtils.defineLazyPreferenceGetter(this, "firstAlertShown",
-      this.kFirstAlertShownPref, false);
-
-    await this.loadStrings();
-    await this.loadBreaches();
-  },
-
-  loadStrings() {
-    let l10nManifest;
-    if (this.extension.rootURI instanceof Ci.nsIJARURI) {
-      l10nManifest = this.extension.rootURI.JARFile
-                            .QueryInterface(Ci.nsIFileURL).file;
-    } else if (this.extension.rootURI instanceof Ci.nsIFileURL) {
-      l10nManifest = this.extension.rootURI.file;
-    }
-
-    if (l10nManifest) {
-      Components.manager.addBootstrappedManifestLocation(l10nManifest);
-
-      XPCOMUtils.defineLazyGetter(this, "strings", () => {
-        return Services.strings.createBundle(
-          "chrome://fxmonitor/locale/fxmonitor.properties");
-      });
-    } else {
-      // Something is very strange if we reach this line, so we throw
-      // in order to prevent init from completing and burst the stack.
-      throw new Error("Cannot find fxmonitor chrome.manifest for registering translated strings");
-    }
-  },
-
-  kRemoteSettingsKey: "fxmonitor-breaches",
-  async loadBreaches() {
-    let populateSites = (data) => {
-      this.domainMap.clear();
-      data.forEach(site => {
-        if (!site.Domain || !site.Name || !site.PwnCount || !site.BreachDate || !site.AddedDate) {
-          Cu.reportError(`Firefox Monitor: malformed breach entry.\nSite:\n${JSON.stringify(site)}`);
-          return;
-        }
-
-        try {
-          this.domainMap.set(site.Domain, {
-            Name: site.Name,
-            PwnCount: site.PwnCount,
-            Year: (new Date(site.BreachDate)).getFullYear(),
-            AddedDate: site.AddedDate.split("T")[0],
-          });
-        } catch (e) {
-          Cu.reportError(`Firefox Monitor: malformed breach entry.\nSite:\n${JSON.stringify(site)}\nError:\n${e}`);
-        }
-      });
-    };
-
-    RemoteSettings(this.kRemoteSettingsKey).on("sync", (event) => {
-      let { data: { current } } = event;
-      populateSites(current);
-    });
-
-    let data = await RemoteSettings(this.kRemoteSettingsKey).get();
-    if (data && data.length) {
-      populateSites(data);
-    }
-  },
-
-  // nsIWebProgressListener implementation.
-  onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-    if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) ||
-        (!aWebProgress.isTopLevel || aWebProgress.isLoadingDocument ||
-         !Components.isSuccessCode(aStatus))) {
-      return;
-    }
-
-    let host;
-    try {
-      host = Services.eTLD.getBaseDomain(aRequest.URI);
-    } catch (e) {
-      // If we can't get the host for the URL, it's not one we
-      // care about for breach alerts anyway.
-      return;
-    }
-
-    this.warnIfNeeded(aBrowser, host);
-  },
-
-  notificationsByWindow: new WeakMap(),
-  panelUIsByWindow: new WeakMap(),
-
-  async startObserving() {
-    if (this.observerAdded) {
-      return;
-    }
-
-    EveryWindow.registerCallback(
-      this.kNotificationID,
-      (win) => {
-        if (this.notificationsByWindow.has(win)) {
-          // We've already set up this window.
-          return;
-        }
-
-        this.notificationsByWindow.set(win, new Set());
-
-        // Start listening across all tabs! The UI will
-        // be set up lazily when we actually need to show
-        // a notification.
-        this.delayedInit().then(() => {
-          win.gBrowser.addTabsProgressListener(this);
-        });
-      },
-      (win, closing) => {
-        // If the window is going away, don't bother doing anything.
-        if (closing) {
-          return;
-        }
-
-        let DOMWindowUtils = win.windowUtils;
-        DOMWindowUtils.removeSheetUsingURIString(this.getURL("privileged/FirefoxMonitor.css"),
-                                                 DOMWindowUtils.AUTHOR_SHEET);
-
-        this.notificationsByWindow.get(win).forEach(n => {
-          n.remove();
-        });
-        this.notificationsByWindow.delete(win);
-
-        let doc = win.document;
-        doc.getElementById(`${this.kNotificationID}-notification-anchor`).remove();
-        doc.getElementById(`${this.kNotificationID}-notification`).remove();
-        this.panelUIsByWindow.delete(win);
-
-        win.gBrowser.removeTabsProgressListener(this);
-      },
-    );
-
-    this.observerAdded = true;
-  },
-
-  setupPanelUI(win) {
-    // Inject our stylesheet.
-    let DOMWindowUtils = win.windowUtils;
-    DOMWindowUtils.loadSheetUsingURIString(this.getURL("privileged/FirefoxMonitor.css"),
-                                           DOMWindowUtils.AUTHOR_SHEET);
-
-    // Setup the popup notification stuff. First, the URL bar icon:
-    let doc = win.document;
-    let notificationBox = doc.getElementById("notification-popup-box");
-    // We create a box to use as the anchor, and put an icon image
-    // inside it. This way, when we animate the icon, its scale change
-    // does not cause the popup notification to bounce due to the anchor
-    // point moving.
-    let anchorBox = doc.createElementNS(XUL_NS, "box");
-    anchorBox.setAttribute("id", `${this.kNotificationID}-notification-anchor`);
-    anchorBox.classList.add("notification-anchor-icon");
-    let img = doc.createElementNS(XUL_NS, "image");
-    img.setAttribute("role", "button");
-    img.classList.add(`${this.kNotificationID}-icon`);
-    img.style.listStyleImage = `url(${this.getURL("assets/monitor32.svg")})`;
-    anchorBox.appendChild(img);
-    notificationBox.appendChild(anchorBox);
-    img.setAttribute("tooltiptext",
-      this.getFormattedString("fxmonitor.anchorIcon.tooltiptext",
-                              [this.getString("fxmonitor.brandName")]));
-
-    // Now, the popupnotificationcontent:
-    let parentElt = doc.defaultView.PopupNotifications.panel.parentNode;
-    let pn = doc.createElementNS(XUL_NS, "popupnotification");
-    let pnContent = doc.createElementNS(XUL_NS, "popupnotificationcontent");
-    let panelUI = new PanelUI(doc);
-    pnContent.appendChild(panelUI.box);
-    pn.appendChild(pnContent);
-    pn.setAttribute("id", `${this.kNotificationID}-notification`);
-    pn.setAttribute("hidden", "true");
-    parentElt.appendChild(pn);
-    this.panelUIsByWindow.set(win, panelUI);
-    return panelUI;
-  },
-
-  stopObserving() {
-    if (!this.observerAdded) {
-      return;
-    }
-
-    EveryWindow.unregisterCallback(this.kNotificationID);
-
-    this.observerAdded = false;
-  },
-
-  warnIfNeeded(browser, host) {
-    if (!this.enabled || this.warnedHostsSet.has(host) || !this.domainMap.has(host)) {
-      return;
-    }
-
-    let site = this.domainMap.get(host);
-
-    // We only alert for breaches that were found up to 2 months ago,
-    // except for the very first alert we show the user - in which case,
-    // we include breaches found in the last three years.
-    let breachDateThreshold = new Date();
-    if (this.firstAlertShown) {
-      breachDateThreshold.setMonth(breachDateThreshold.getMonth() - 2);
-    } else {
-      breachDateThreshold.setFullYear(breachDateThreshold.getFullYear() - 1);
-    }
-
-    if (new Date(site.AddedDate).getTime() < breachDateThreshold.getTime()) {
-      return;
-    } else if (!this.firstAlertShown) {
-      Preferences.set(this.kFirstAlertShownPref, true);
-    }
-
-    this.warnedHostsSet.add(host);
-    Preferences.set(this.kWarnedHostsPref, JSON.stringify([...this.warnedHostsSet]));
-
-    let doc = browser.ownerDocument;
-    let win = doc.defaultView;
-    let panelUI = this.panelUIsByWindow.get(win);
-    if (!panelUI) {
-      panelUI = this.setupPanelUI(win);
-    }
-
-    let animatedOnce = false;
-    let populatePanel = (event) => {
-      switch (event) {
-        case "showing":
-          panelUI.refresh(site);
-          if (animatedOnce) {
-            // If we've already animated once for this site, don't animate again.
-            doc.getElementById("notification-popup")
-               .setAttribute("fxmonitoranimationdone", "true");
-            doc.getElementById(`${this.kNotificationID}-notification-anchor`)
-               .setAttribute("fxmonitoranimationdone", "true");
-            break;
-          }
-          // Make sure we animate if we're coming from another tab that has
-          // this attribute set.
-          doc.getElementById("notification-popup")
-             .removeAttribute("fxmonitoranimationdone");
-          doc.getElementById(`${this.kNotificationID}-notification-anchor`)
-             .removeAttribute("fxmonitoranimationdone");
-          break;
-        case "shown":
-          animatedOnce = true;
-          break;
-        case "removed":
-          this.notificationsByWindow.get(win).delete(
-            win.PopupNotifications.getNotification(this.kNotificationID, browser));
-          Services.telemetry.recordEvent("fxmonitor", "interaction", "doorhanger_removed");
-          break;
-      }
-    };
-
-    let n = win.PopupNotifications.show(
-      browser, this.kNotificationID, "",
-      `${this.kNotificationID}-notification-anchor`,
-      panelUI.primaryAction, panelUI.secondaryActions, {
-        persistent: true,
-        hideClose: true,
-        eventCallback: populatePanel,
-        popupIconURL: this.getURL("assets/monitor32.svg"),
-      }
-    );
-
-    Services.telemetry.recordEvent("fxmonitor", "interaction", "doorhanger_shown");
-
-    this.notificationsByWindow.get(win).add(n);
-  },
-};
-
-function PanelUI(doc) {
-  this.site = null;
-  this.doc = doc;
-
-  let box = doc.createElementNS(XUL_NS, "vbox");
-
-  let elt = doc.createElementNS(XUL_NS, "description");
-  elt.textContent = this.getString("fxmonitor.popupHeader");
-  elt.classList.add("headerText");
-  box.appendChild(elt);
-
-  elt = doc.createElementNS(XUL_NS, "description");
-  elt.classList.add("popupText");
-  box.appendChild(elt);
-
-  this.box = box;
-}
-
-PanelUI.prototype = {
-  getString(aKey) {
-    return FirefoxMonitor.getString(aKey);
-  },
-
-  getFormattedString(aKey, args) {
-    return FirefoxMonitor.getFormattedString(aKey, args);
-  },
-
-  get brandString() {
-    if (this._brandString) {
-      return this._brandString;
-    }
-    return this._brandString = this.getString("fxmonitor.brandName");
-  },
-
-  getFirefoxMonitorURL: (aSiteName) => {
-    return `${FirefoxMonitor.FirefoxMonitorURL}/?breach=${encodeURIComponent(aSiteName)}&utm_source=firefox&utm_medium=popup`;
-  },
-
-  get primaryAction() {
-    if (this._primaryAction) {
-      return this._primaryAction;
-    }
-    return this._primaryAction = {
-      label: this.getFormattedString("fxmonitor.checkButton.label", [this.brandString]),
-      accessKey: this.getString("fxmonitor.checkButton.accessKey"),
-      callback: () => {
-        let win = this.doc.defaultView;
-        win.openTrustedLinkIn(
-          this.getFirefoxMonitorURL(this.site.Name), "tab", { });
-
-        Services.telemetry.recordEvent("fxmonitor", "interaction", "check_btn");
-      },
-    };
-  },
-
-  get secondaryActions() {
-    if (this._secondaryActions) {
-      return this._secondaryActions;
-    }
-    return this._secondaryActions = [
-      {
-        label: this.getString("fxmonitor.dismissButton.label"),
-        accessKey: this.getString("fxmonitor.dismissButton.accessKey"),
-        callback: () => {
-          Services.telemetry.recordEvent("fxmonitor", "interaction", "dismiss_btn");
-        },
-      }, {
-        label: this.getFormattedString("fxmonitor.neverShowButton.label", [this.brandString]),
-        accessKey: this.getString("fxmonitor.neverShowButton.accessKey"),
-        callback: () => {
-          FirefoxMonitor.disable();
-          Services.telemetry.recordEvent("fxmonitor", "interaction", "never_show_btn");
-        },
-      },
-    ];
-  },
-
-  refresh(site) {
-    this.site = site;
-
-    let elt = this.box.querySelector(".popupText");
-
-    // If > 100k, the PwnCount is rounded down to the most significant
-    // digit and prefixed with "More than".
-    // Ex.: 12,345 -> 12,345
-    //      234,567 -> More than 200,000
-    //      345,678,901 -> More than 300,000,000
-    //      4,567,890,123 -> More than 4,000,000,000
-    let k100k = 100000;
-    let pwnCount = site.PwnCount;
-    let stringName = "fxmonitor.popupText";
-    if (pwnCount > k100k) {
-      let multiplier = 1;
-      while (pwnCount >= 10) {
-        pwnCount /= 10;
-        multiplier *= 10;
-      }
-      pwnCount = Math.floor(pwnCount) * multiplier;
-      stringName = "fxmonitor.popupTextRounded";
-    }
-
-    elt.textContent =
-      PluralForm.get(pwnCount, this.getString(stringName))
-                .replace("#1", pwnCount.toLocaleString())
-                .replace("#2", site.Name)
-                .replace("#3", site.Year)
-                .replace("#4", this.brandString);
-  },
-};
--- a/browser/extensions/fxmonitor/privileged/api.js
+++ b/browser/extensions/fxmonitor/privileged/api.js
@@ -1,32 +1,547 @@
 /* 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/. */
 
-/* globals ExtensionAPI */
+/* globals ExtensionAPI, XPCOMUtils */
 
+ChromeUtils.defineModuleGetter(this, "EveryWindow",
+                               "resource:///modules/EveryWindow.jsm");
+ChromeUtils.defineModuleGetter(this, "PluralForm",
+                               "resource://gre/modules/PluralForm.jsm");
+ChromeUtils.defineModuleGetter(this, "Preferences",
+                               "resource://gre/modules/Preferences.jsm");
+ChromeUtils.defineModuleGetter(this, "RemoteSettings",
+                               "resource://services-settings/remote-settings.js");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
-let FirefoxMonitorContainer = {};
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 this.fxmonitor = class extends ExtensionAPI {
   getAPI(context) {
-    Services.scriptloader.loadSubScript(context.extension.getURL("privileged/FirefoxMonitor.jsm"),
-                                        FirefoxMonitorContainer);
     return {
       fxmonitor: {
         async start() {
-          await FirefoxMonitorContainer.FirefoxMonitor.init(context.extension);
+          await FirefoxMonitor.init(context.extension);
         },
       },
     };
   }
 
   onShutdown(shutdownReason) {
-    if (Services.startup.shuttingDown || !FirefoxMonitorContainer.FirefoxMonitor) {
+    if (Services.startup.shuttingDown) {
+      return;
+    }
+
+    FirefoxMonitor.stopObserving();
+  }
+};
+
+this.FirefoxMonitor = {
+  // Map of breached site host -> breach metadata.
+  domainMap: new Map(),
+
+  // Set of hosts for which the user has already been shown,
+  // and interacted with, the popup.
+  warnedHostsSet: new Set(),
+
+  // The above set is persisted as a JSON string in this pref.
+  kWarnedHostsPref: "extensions.fxmonitor.warnedHosts",
+
+  // Reference to the extension object from the WebExtension context.
+  // Used for getting URIs for resources packaged in the extension.
+  extension: null,
+
+  // Whether we've started observing for the user visiting a breached site.
+  observerAdded: false,
+
+  // This is here for documentation, will be redefined to a lazy getter
+  // that creates and returns a string bundle in loadStrings().
+  strings: null,
+
+  // This is here for documentation, will be redefined to a pref getter
+  // using XPCOMUtils.defineLazyPreferenceGetter in init().
+  enabled: null,
+
+  kEnabledPref: "extensions.fxmonitor.enabled",
+
+  // Telemetry event recording is enabled by default.
+  // If this pref exists and is true-y, it's disabled.
+  kTelemetryDisabledPref: "extensions.fxmonitor.telemetryDisabled",
+
+  kNotificationID: "fxmonitor",
+
+  // This is here for documentation, will be redefined to a pref getter
+  // using XPCOMUtils.defineLazyPreferenceGetter in delayedInit().
+  // The value of this property is used as the URL to which the user
+  // is directed when they click "Check Firefox Monitor".
+  FirefoxMonitorURL: null,
+  kFirefoxMonitorURLPref: "extensions.fxmonitor.FirefoxMonitorURL",
+  kDefaultFirefoxMonitorURL: "https://monitor.firefox.com",
+
+  // This is here for documentation, will be redefined to a pref getter
+  // using XPCOMUtils.defineLazyPreferenceGetter in delayedInit().
+  // The pref stores whether the user has seen a breach alert already.
+  // The value is used in warnIfNeeded.
+  firstAlertShown: null,
+  kFirstAlertShownPref: "extensions.fxmonitor.firstAlertShown",
+
+  disable() {
+    Preferences.set(this.kEnabledPref, false);
+  },
+
+  getURL(aPath) {
+    return this.extension.getURL(aPath);
+  },
+
+  getString(aKey) {
+    return this.strings.GetStringFromName(aKey);
+  },
+
+  getFormattedString(aKey, args) {
+    return this.strings.formatStringFromName(aKey, args, args.length);
+  },
+
+  init(aExtension) {
+    this.extension = aExtension;
+
+    XPCOMUtils.defineLazyPreferenceGetter(
+      this, "enabled", this.kEnabledPref, true,
+      (pref, oldVal, newVal) => {
+        if (newVal) {
+          this.startObserving();
+        } else {
+          this.stopObserving();
+        }
+      }
+    );
+
+    if (this.enabled) {
+      this.startObserving();
+    }
+  },
+
+  // Used to enforce idempotency of delayedInit. delayedInit is
+  // called in startObserving() to ensure we load our strings, etc.
+  _delayedInited: false,
+  async delayedInit() {
+    if (this._delayedInited) {
+      return;
+    }
+
+    this._delayedInited = true;
+
+    // Expire our telemetry on November 1, at which time
+    // we should redo data-review.
+    let telemetryExpiryDate = new Date(2019, 10, 1); // Month is zero-index
+    let today = new Date();
+    let expired = today.getTime() > telemetryExpiryDate.getTime();
+
+    Services.telemetry.registerEvents("fxmonitor", {
+      "interaction": {
+        methods: ["interaction"],
+        objects: [
+          "doorhanger_shown",
+          "doorhanger_removed",
+          "check_btn",
+          "dismiss_btn",
+          "never_show_btn",
+        ],
+        record_on_release: true,
+        expired,
+      },
+    });
+
+    let telemetryEnabled = !Preferences.get(this.kTelemetryDisabledPref);
+    Services.telemetry.setEventRecordingEnabled("fxmonitor", telemetryEnabled);
+
+    let warnedHostsJSON = Preferences.get(this.kWarnedHostsPref, "");
+    if (warnedHostsJSON) {
+      try {
+        let json = JSON.parse(warnedHostsJSON);
+        this.warnedHostsSet = new Set(json);
+      } catch (ex) {
+        // Invalid JSON, invalidate the pref.
+        Preferences.reset(this.kWarnedHostsPref);
+      }
+    }
+
+    XPCOMUtils.defineLazyPreferenceGetter(this, "FirefoxMonitorURL",
+      this.kFirefoxMonitorURLPref, this.kDefaultFirefoxMonitorURL);
+
+    XPCOMUtils.defineLazyPreferenceGetter(this, "firstAlertShown",
+      this.kFirstAlertShownPref, false);
+
+    await this.loadStrings();
+    await this.loadBreaches();
+  },
+
+  loadStrings() {
+    let l10nManifest;
+    if (this.extension.rootURI instanceof Ci.nsIJARURI) {
+      l10nManifest = this.extension.rootURI.JARFile
+                            .QueryInterface(Ci.nsIFileURL).file;
+    } else if (this.extension.rootURI instanceof Ci.nsIFileURL) {
+      l10nManifest = this.extension.rootURI.file;
+    }
+
+    if (l10nManifest) {
+      Components.manager.addBootstrappedManifestLocation(l10nManifest);
+
+      XPCOMUtils.defineLazyGetter(this, "strings", () => {
+        return Services.strings.createBundle(
+          "chrome://fxmonitor/locale/fxmonitor.properties");
+      });
+    } else {
+      // Something is very strange if we reach this line, so we throw
+      // in order to prevent init from completing and burst the stack.
+      throw new Error("Cannot find fxmonitor chrome.manifest for registering translated strings");
+    }
+  },
+
+  kRemoteSettingsKey: "fxmonitor-breaches",
+  async loadBreaches() {
+    let populateSites = (data) => {
+      this.domainMap.clear();
+      data.forEach(site => {
+        if (!site.Domain || !site.Name || !site.PwnCount || !site.BreachDate || !site.AddedDate) {
+          Cu.reportError(`Firefox Monitor: malformed breach entry.\nSite:\n${JSON.stringify(site)}`);
+          return;
+        }
+
+        try {
+          this.domainMap.set(site.Domain, {
+            Name: site.Name,
+            PwnCount: site.PwnCount,
+            Year: (new Date(site.BreachDate)).getFullYear(),
+            AddedDate: site.AddedDate.split("T")[0],
+          });
+        } catch (e) {
+          Cu.reportError(`Firefox Monitor: malformed breach entry.\nSite:\n${JSON.stringify(site)}\nError:\n${e}`);
+        }
+      });
+    };
+
+    RemoteSettings(this.kRemoteSettingsKey).on("sync", (event) => {
+      let { data: { current } } = event;
+      populateSites(current);
+    });
+
+    let data = await RemoteSettings(this.kRemoteSettingsKey).get();
+    if (data && data.length) {
+      populateSites(data);
+    }
+  },
+
+  // nsIWebProgressListener implementation.
+  onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+    if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) ||
+        (!aWebProgress.isTopLevel || aWebProgress.isLoadingDocument ||
+         !Components.isSuccessCode(aStatus))) {
+      return;
+    }
+
+    let host;
+    try {
+      host = Services.eTLD.getBaseDomain(aRequest.URI);
+    } catch (e) {
+      // If we can't get the host for the URL, it's not one we
+      // care about for breach alerts anyway.
+      return;
+    }
+
+    this.warnIfNeeded(aBrowser, host);
+  },
+
+  notificationsByWindow: new WeakMap(),
+  panelUIsByWindow: new WeakMap(),
+
+  async startObserving() {
+    if (this.observerAdded) {
       return;
     }
 
-    FirefoxMonitorContainer.FirefoxMonitor.stopObserving();
-  }
+    EveryWindow.registerCallback(
+      this.kNotificationID,
+      (win) => {
+        if (this.notificationsByWindow.has(win)) {
+          // We've already set up this window.
+          return;
+        }
+
+        this.notificationsByWindow.set(win, new Set());
+
+        // Start listening across all tabs! The UI will
+        // be set up lazily when we actually need to show
+        // a notification.
+        this.delayedInit().then(() => {
+          win.gBrowser.addTabsProgressListener(this);
+        });
+      },
+      (win, closing) => {
+        // If the window is going away, don't bother doing anything.
+        if (closing) {
+          return;
+        }
+
+        let DOMWindowUtils = win.windowUtils;
+        DOMWindowUtils.removeSheetUsingURIString(this.getURL("privileged/FirefoxMonitor.css"),
+                                                 DOMWindowUtils.AUTHOR_SHEET);
+
+        this.notificationsByWindow.get(win).forEach(n => {
+          n.remove();
+        });
+        this.notificationsByWindow.delete(win);
+
+        let doc = win.document;
+        doc.getElementById(`${this.kNotificationID}-notification-anchor`).remove();
+        doc.getElementById(`${this.kNotificationID}-notification`).remove();
+        this.panelUIsByWindow.delete(win);
+
+        win.gBrowser.removeTabsProgressListener(this);
+      },
+    );
+
+    this.observerAdded = true;
+  },
+
+  setupPanelUI(win) {
+    // Inject our stylesheet.
+    let DOMWindowUtils = win.windowUtils;
+    DOMWindowUtils.loadSheetUsingURIString(this.getURL("privileged/FirefoxMonitor.css"),
+                                           DOMWindowUtils.AUTHOR_SHEET);
+
+    // Setup the popup notification stuff. First, the URL bar icon:
+    let doc = win.document;
+    let notificationBox = doc.getElementById("notification-popup-box");
+    // We create a box to use as the anchor, and put an icon image
+    // inside it. This way, when we animate the icon, its scale change
+    // does not cause the popup notification to bounce due to the anchor
+    // point moving.
+    let anchorBox = doc.createElementNS(XUL_NS, "box");
+    anchorBox.setAttribute("id", `${this.kNotificationID}-notification-anchor`);
+    anchorBox.classList.add("notification-anchor-icon");
+    let img = doc.createElementNS(XUL_NS, "image");
+    img.setAttribute("role", "button");
+    img.classList.add(`${this.kNotificationID}-icon`);
+    img.style.listStyleImage = `url(${this.getURL("assets/monitor32.svg")})`;
+    anchorBox.appendChild(img);
+    notificationBox.appendChild(anchorBox);
+    img.setAttribute("tooltiptext",
+      this.getFormattedString("fxmonitor.anchorIcon.tooltiptext",
+                              [this.getString("fxmonitor.brandName")]));
+
+    // Now, the popupnotificationcontent:
+    let parentElt = doc.defaultView.PopupNotifications.panel.parentNode;
+    let pn = doc.createElementNS(XUL_NS, "popupnotification");
+    let pnContent = doc.createElementNS(XUL_NS, "popupnotificationcontent");
+    let panelUI = new PanelUI(doc);
+    pnContent.appendChild(panelUI.box);
+    pn.appendChild(pnContent);
+    pn.setAttribute("id", `${this.kNotificationID}-notification`);
+    pn.setAttribute("hidden", "true");
+    parentElt.appendChild(pn);
+    this.panelUIsByWindow.set(win, panelUI);
+    return panelUI;
+  },
+
+  stopObserving() {
+    if (!this.observerAdded) {
+      return;
+    }
+
+    EveryWindow.unregisterCallback(this.kNotificationID);
+
+    this.observerAdded = false;
+  },
+
+  warnIfNeeded(browser, host) {
+    if (!this.enabled || this.warnedHostsSet.has(host) || !this.domainMap.has(host)) {
+      return;
+    }
+
+    let site = this.domainMap.get(host);
+
+    // We only alert for breaches that were found up to 2 months ago,
+    // except for the very first alert we show the user - in which case,
+    // we include breaches found in the last three years.
+    let breachDateThreshold = new Date();
+    if (this.firstAlertShown) {
+      breachDateThreshold.setMonth(breachDateThreshold.getMonth() - 2);
+    } else {
+      breachDateThreshold.setFullYear(breachDateThreshold.getFullYear() - 1);
+    }
+
+    if (new Date(site.AddedDate).getTime() < breachDateThreshold.getTime()) {
+      return;
+    } else if (!this.firstAlertShown) {
+      Preferences.set(this.kFirstAlertShownPref, true);
+    }
+
+    this.warnedHostsSet.add(host);
+    Preferences.set(this.kWarnedHostsPref, JSON.stringify([...this.warnedHostsSet]));
+
+    let doc = browser.ownerDocument;
+    let win = doc.defaultView;
+    let panelUI = this.panelUIsByWindow.get(win);
+    if (!panelUI) {
+      panelUI = this.setupPanelUI(win);
+    }
+
+    let animatedOnce = false;
+    let populatePanel = (event) => {
+      switch (event) {
+        case "showing":
+          panelUI.refresh(site);
+          if (animatedOnce) {
+            // If we've already animated once for this site, don't animate again.
+            doc.getElementById("notification-popup")
+               .setAttribute("fxmonitoranimationdone", "true");
+            doc.getElementById(`${this.kNotificationID}-notification-anchor`)
+               .setAttribute("fxmonitoranimationdone", "true");
+            break;
+          }
+          // Make sure we animate if we're coming from another tab that has
+          // this attribute set.
+          doc.getElementById("notification-popup")
+             .removeAttribute("fxmonitoranimationdone");
+          doc.getElementById(`${this.kNotificationID}-notification-anchor`)
+             .removeAttribute("fxmonitoranimationdone");
+          break;
+        case "shown":
+          animatedOnce = true;
+          break;
+        case "removed":
+          this.notificationsByWindow.get(win).delete(
+            win.PopupNotifications.getNotification(this.kNotificationID, browser));
+          Services.telemetry.recordEvent("fxmonitor", "interaction", "doorhanger_removed");
+          break;
+      }
+    };
+
+    let n = win.PopupNotifications.show(
+      browser, this.kNotificationID, "",
+      `${this.kNotificationID}-notification-anchor`,
+      panelUI.primaryAction, panelUI.secondaryActions, {
+        persistent: true,
+        hideClose: true,
+        eventCallback: populatePanel,
+        popupIconURL: this.getURL("assets/monitor32.svg"),
+      }
+    );
+
+    Services.telemetry.recordEvent("fxmonitor", "interaction", "doorhanger_shown");
+
+    this.notificationsByWindow.get(win).add(n);
+  },
 };
+
+function PanelUI(doc) {
+  this.site = null;
+  this.doc = doc;
+
+  let box = doc.createElementNS(XUL_NS, "vbox");
+
+  let elt = doc.createElementNS(XUL_NS, "description");
+  elt.textContent = this.getString("fxmonitor.popupHeader");
+  elt.classList.add("headerText");
+  box.appendChild(elt);
+
+  elt = doc.createElementNS(XUL_NS, "description");
+  elt.classList.add("popupText");
+  box.appendChild(elt);
+
+  this.box = box;
+}
+
+PanelUI.prototype = {
+  getString(aKey) {
+    return FirefoxMonitor.getString(aKey);
+  },
+
+  getFormattedString(aKey, args) {
+    return FirefoxMonitor.getFormattedString(aKey, args);
+  },
+
+  get brandString() {
+    if (this._brandString) {
+      return this._brandString;
+    }
+    return this._brandString = this.getString("fxmonitor.brandName");
+  },
+
+  getFirefoxMonitorURL: (aSiteName) => {
+    return `${FirefoxMonitor.FirefoxMonitorURL}/?breach=${encodeURIComponent(aSiteName)}&utm_source=firefox&utm_medium=popup`;
+  },
+
+  get primaryAction() {
+    if (this._primaryAction) {
+      return this._primaryAction;
+    }
+    return this._primaryAction = {
+      label: this.getFormattedString("fxmonitor.checkButton.label", [this.brandString]),
+      accessKey: this.getString("fxmonitor.checkButton.accessKey"),
+      callback: () => {
+        let win = this.doc.defaultView;
+        win.openTrustedLinkIn(
+          this.getFirefoxMonitorURL(this.site.Name), "tab", { });
+
+        Services.telemetry.recordEvent("fxmonitor", "interaction", "check_btn");
+      },
+    };
+  },
+
+  get secondaryActions() {
+    if (this._secondaryActions) {
+      return this._secondaryActions;
+    }
+    return this._secondaryActions = [
+      {
+        label: this.getString("fxmonitor.dismissButton.label"),
+        accessKey: this.getString("fxmonitor.dismissButton.accessKey"),
+        callback: () => {
+          Services.telemetry.recordEvent("fxmonitor", "interaction", "dismiss_btn");
+        },
+      }, {
+        label: this.getFormattedString("fxmonitor.neverShowButton.label", [this.brandString]),
+        accessKey: this.getString("fxmonitor.neverShowButton.accessKey"),
+        callback: () => {
+          FirefoxMonitor.disable();
+          Services.telemetry.recordEvent("fxmonitor", "interaction", "never_show_btn");
+        },
+      },
+    ];
+  },
+
+  refresh(site) {
+    this.site = site;
+
+    let elt = this.box.querySelector(".popupText");
+
+    // If > 100k, the PwnCount is rounded down to the most significant
+    // digit and prefixed with "More than".
+    // Ex.: 12,345 -> 12,345
+    //      234,567 -> More than 200,000
+    //      345,678,901 -> More than 300,000,000
+    //      4,567,890,123 -> More than 4,000,000,000
+    let k100k = 100000;
+    let pwnCount = site.PwnCount;
+    let stringName = "fxmonitor.popupText";
+    if (pwnCount > k100k) {
+      let multiplier = 1;
+      while (pwnCount >= 10) {
+        pwnCount /= 10;
+        multiplier *= 10;
+      }
+      pwnCount = Math.floor(pwnCount) * multiplier;
+      stringName = "fxmonitor.popupTextRounded";
+    }
+
+    elt.textContent =
+      PluralForm.get(pwnCount, this.getString(stringName))
+                .replace("#1", pwnCount.toLocaleString())
+                .replace("#2", site.Name)
+                .replace("#3", site.Year)
+                .replace("#4", this.brandString);
+  },
+};