diff options
Diffstat (limited to 'browser/components/fxmonitor')
-rw-r--r-- | browser/components/fxmonitor/FirefoxMonitor.jsm | 633 | ||||
-rw-r--r-- | browser/components/fxmonitor/content/FirefoxMonitor.css | 90 | ||||
-rw-r--r-- | browser/components/fxmonitor/content/monitor32.svg | 6 | ||||
-rw-r--r-- | browser/components/fxmonitor/jar.mn | 8 | ||||
-rw-r--r-- | browser/components/fxmonitor/moz.build | 16 | ||||
-rw-r--r-- | browser/components/fxmonitor/test/browser/browser.ini | 3 | ||||
-rw-r--r-- | browser/components/fxmonitor/test/browser/browser_fxmonitor_doorhanger.js | 254 |
7 files changed, 1010 insertions, 0 deletions
diff --git a/browser/components/fxmonitor/FirefoxMonitor.jsm b/browser/components/fxmonitor/FirefoxMonitor.jsm new file mode 100644 index 0000000000..a8f4ac98b7 --- /dev/null +++ b/browser/components/fxmonitor/FirefoxMonitor.jsm @@ -0,0 +1,633 @@ +/* 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"; + +const EXPORTED_SYMBOLS = ["FirefoxMonitor"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + EveryWindow: "resource:///modules/EveryWindow.jsm", + PluralForm: "resource://gre/modules/PluralForm.jsm", + Preferences: "resource://gre/modules/Preferences.jsm", + RemoteSettings: "resource://services-settings/remote-settings.js", + Services: "resource://gre/modules/Services.jsm", +}); + +const STYLESHEET = "chrome://browser/content/fxmonitor/FirefoxMonitor.css"; +const ICON = "chrome://browser/content/fxmonitor/monitor32.svg"; + +this.FirefoxMonitor = { + // Map of breached site host -> breach metadata. + domainMap: new Map(), + + // 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", + + // This is here for documentation, will be redefined to a pref getter + // using XPCOMUtils.defineLazyPreferenceGetter in delayedInit(). + // Telemetry event recording is enabled by default. + // If this pref exists and is true-y, it's disabled. + telemetryDisabled: null, + 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); + }, + + getString(aKey) { + return this.strings.GetStringFromName(aKey); + }, + + getFormattedString(aKey, args) { + return this.strings.formatStringFromName(aKey, args); + }, + + // We used to persist the list of hosts we've already warned the + // user for in this pref. Now, we check the pref at init and + // if it has a value, migrate the remembered hosts to content prefs + // and clear this one. + kWarnedHostsPref: "extensions.fxmonitor.warnedHosts", + migrateWarnedHostsIfNeeded() { + if (!Preferences.isSet(this.kWarnedHostsPref)) { + return; + } + + let hosts = []; + try { + hosts = JSON.parse(Preferences.get(this.kWarnedHostsPref)); + } catch (ex) { + // Invalid JSON, nothing to be done. + } + + let loadContext = Cu.createLoadContext(); + for (let host of hosts) { + this.rememberWarnedHost(loadContext, host); + } + + Preferences.reset(this.kWarnedHostsPref); + }, + + init() { + 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; + + XPCOMUtils.defineLazyServiceGetter( + this, + "_contentPrefService", + "@mozilla.org/content-pref/service;1", + "nsIContentPrefService2" + ); + + this.migrateWarnedHostsIfNeeded(); + + // 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); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "FirefoxMonitorURL", + this.kFirefoxMonitorURLPref, + this.kDefaultFirefoxMonitorURL + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "firstAlertShown", + this.kFirstAlertShownPref, + false + ); + + XPCOMUtils.defineLazyGetter(this, "strings", () => { + return Services.strings.createBundle( + "chrome://browser/locale/fxmonitor.properties" + ); + }); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "telemetryDisabled", + this.kTelemetryDisabledPref, + false + ); + + await this.loadBreaches(); + }, + + 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( + STYLESHEET, + DOMWindowUtils.AUTHOR_SHEET + ); + + if (this.notificationsByWindow.has(win)) { + this.notificationsByWindow.get(win).forEach(n => { + n.remove(); + }); + this.notificationsByWindow.delete(win); + } + + if (this.panelUIsByWindow.has(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( + STYLESHEET, + 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.createXULElement("box"); + anchorBox.setAttribute("id", `${this.kNotificationID}-notification-anchor`); + anchorBox.classList.add("notification-anchor-icon"); + let img = doc.createXULElement("image"); + img.setAttribute("role", "button"); + img.classList.add(`${this.kNotificationID}-icon`); + img.style.listStyleImage = `url(${ICON})`; + 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.createXULElement("popupnotification"); + let pnContent = doc.createXULElement("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; + }, + + async hostAlreadyWarned(loadContext, host) { + return new Promise((resolve, reject) => { + this._contentPrefService.getByDomainAndName( + host, + "extensions.fxmonitor.hostAlreadyWarned", + loadContext, + { + handleCompletion: () => resolve(false), + handleResult: result => resolve(result.value), + } + ); + }); + }, + + rememberWarnedHost(loadContext, host) { + this._contentPrefService.set( + host, + "extensions.fxmonitor.hostAlreadyWarned", + true, + loadContext + ); + }, + + async warnIfNeeded(browser, host) { + if ( + !this.enabled || + !this.domainMap.has(host) || + (await this.hostAlreadyWarned(browser.loadContext, 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.rememberWarnedHost(browser.loadContext, host); + + 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 + ) + ); + this.recordEvent("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: ICON, + } + ); + + this.recordEvent("doorhanger_shown"); + + this.notificationsByWindow.get(win).add(n); + }, + + recordEvent(aEventName) { + if (this.telemetryDisabled) { + return; + } + + Services.telemetry.recordEvent("fxmonitor", "interaction", aEventName); + }, +}; + +function PanelUI(doc) { + this.site = null; + this.doc = doc; + + let box = doc.createXULElement("vbox"); + + let elt = doc.createXULElement("description"); + elt.textContent = this.getString("fxmonitor.popupHeader"); + elt.classList.add("headerText"); + box.appendChild(elt); + + elt = doc.createXULElement("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", + {} + ); + + FirefoxMonitor.recordEvent("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: () => { + FirefoxMonitor.recordEvent("dismiss_btn"); + }, + }, + { + label: this.getFormattedString("fxmonitor.neverShowButton.label", [ + this.brandString, + ]), + accessKey: this.getString("fxmonitor.neverShowButton.accessKey"), + callback: () => { + FirefoxMonitor.disable(); + FirefoxMonitor.recordEvent("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); + }, +}; diff --git a/browser/components/fxmonitor/content/FirefoxMonitor.css b/browser/components/fxmonitor/content/FirefoxMonitor.css new file mode 100644 index 0000000000..69b6975781 --- /dev/null +++ b/browser/components/fxmonitor/content/FirefoxMonitor.css @@ -0,0 +1,90 @@ +/* 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/. */ + +#fxmonitor-notification popupnotificationcontent { + margin-top: 0; +} + +#fxmonitor-notification .popup-notification-body > :not(popupnotificationcontent) { + display: none; +} + +.fxmonitor-icon { + width: 16px; + height: 16px; +} + +#fxmonitor-notification-anchor, +.fxmonitor-icon { + animation-timing-function: linear; + animation-duration: 0.66s; +} + +/* We only want to animate the icon/doorhanger the first time it's shown for a site. + An attribute fxmonitoranimationdone is used to control this from FirefoxMonitor.jsm */ +#fxmonitor-notification-anchor:not([fxmonitoranimationdone]) { + animation-name: fxmonitor-anchor-animation; +} + +#fxmonitor-notification-anchor:not([fxmonitoranimationdone]):-moz-locale-dir(rtl) { + animation-name: fxmonitor-anchor-animation-rtl; +} + +#fxmonitor-notification-anchor:not([fxmonitoranimationdone]) .fxmonitor-icon { + animation-name: fxmonitor-icon-animation; +} + +#notification-popup[popupid=fxmonitor]:not([fxmonitoranimationdone]) { + transition-delay: 0.33s; +} + +/* Animate the appearance of the anchor icon: push the other icons to the right. */ +@keyframes fxmonitor-anchor-animation { + from { + margin-right: -20px; + } + 50% { + margin-right: 0; + } + to { + } +} + +/* For RTL locales, push the other icons to the left. */ +@keyframes fxmonitor-anchor-animation-rtl { + from { + margin-left: -20px; + } + 50% { + margin-left: 0; + } + to { + } +} + +/* After the appearance of the anchor box, expand the icon into view */ +@keyframes fxmonitor-icon-animation { + from { + transform: scale(0); + opacity: 0; + } + 50% { + transform: scale(0); + opacity: 0; + } + 75% { + transform: scale(1.2); + } + to { + } +} + +#fxmonitor-notification .popupText { + max-width: 300px; +} + +#fxmonitor-notification .headerText { + font-weight: 600; + white-space: pre; +} diff --git a/browser/components/fxmonitor/content/monitor32.svg b/browser/components/fxmonitor/content/monitor32.svg new file mode 100644 index 0000000000..ffc3902ad5 --- /dev/null +++ b/browser/components/fxmonitor/content/monitor32.svg @@ -0,0 +1,6 @@ +<!-- 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/. --> +<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="context-fill" fill-opacity="context-fill-opacity" fill-rule="nonzero"> + <path d="M27.188 6.714L24 4.874 16.738.684l-.227-.13a3.866 3.866 0 0 0-3.846 0l-.228.13-10.228 5.9-.227.13a3.859 3.859 0 0 0-1.927 3.335V22.382c0 1.372.739 2.646 1.927 3.335l10.449 6.03a1.608 1.608 0 0 0 2.19-.585 1.606 1.606 0 0 0-.584-2.19L3.815 23.071a1.103 1.103 0 0 1-.547-.954V10.314c0-.394.209-.757.547-.954l1.81-1.046 8.418-4.862c.339-.197.757-.19 1.095 0l10.228 5.902c.339.197.548.56.548.954V22.11c0 .394-.21.757-.548.954l-3.458 2-1.748-2.653a8.317 8.317 0 0 0 2.77-6.197c0-4.597-3.742-8.338-8.34-8.338-4.596 0-8.338 3.741-8.338 8.338 0 4.597 3.736 8.339 8.333 8.339.984 0 1.932-.172 2.812-.492l2.652 4.03c.043.062.086.123.136.179.006.012.018.018.03.03.056.062.117.117.179.167.018.012.03.024.05.037.073.055.147.098.227.141.018.006.037.019.055.025.068.03.142.061.216.08.018.006.03.012.049.012.086.025.172.037.258.043.025 0 .05.006.074.006.025 0 .05.006.074.006.05 0 .098-.006.148-.012.024 0 .043 0 .067-.006a1.43 1.43 0 0 0 .271-.062c.025-.006.05-.018.068-.024.067-.025.135-.056.203-.092.012-.007.03-.013.043-.019l4.997-2.886a3.859 3.859 0 0 0 1.926-3.335V10.049a3.885 3.885 0 0 0-1.932-3.335zM9.452 16.215a5.137 5.137 0 0 1 5.133-5.132 5.137 5.137 0 0 1 5.132 5.132 5.137 5.137 0 0 1-5.132 5.133 5.137 5.137 0 0 1-5.133-5.133z"/> +</svg> diff --git a/browser/components/fxmonitor/jar.mn b/browser/components/fxmonitor/jar.mn new file mode 100644 index 0000000000..c21d8ac6fb --- /dev/null +++ b/browser/components/fxmonitor/jar.mn @@ -0,0 +1,8 @@ +#filter substitution +# 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/. + +browser.jar: + content/browser/fxmonitor/FirefoxMonitor.css (content/FirefoxMonitor.css) + content/browser/fxmonitor/monitor32.svg (content/monitor32.svg) diff --git a/browser/components/fxmonitor/moz.build b/browser/components/fxmonitor/moz.build new file mode 100644 index 0000000000..1315cd4f5b --- /dev/null +++ b/browser/components/fxmonitor/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Firefox Monitor") + +EXTRA_JS_MODULES += [ + "FirefoxMonitor.jsm", +] + +JAR_MANIFESTS += ["jar.mn"] + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"] diff --git a/browser/components/fxmonitor/test/browser/browser.ini b/browser/components/fxmonitor/test/browser/browser.ini new file mode 100644 index 0000000000..613bd0e1af --- /dev/null +++ b/browser/components/fxmonitor/test/browser/browser.ini @@ -0,0 +1,3 @@ +[browser_fxmonitor_doorhanger.js] +skip-if = debug # bug 1547517 +tags = remote-settings diff --git a/browser/components/fxmonitor/test/browser/browser_fxmonitor_doorhanger.js b/browser/components/fxmonitor/test/browser/browser_fxmonitor_doorhanger.js new file mode 100644 index 0000000000..ccc2aa9e1d --- /dev/null +++ b/browser/components/fxmonitor/test/browser/browser_fxmonitor_doorhanger.js @@ -0,0 +1,254 @@ +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "RemoteSettings", + "resource://services-settings/remote-settings.js" +); + +const kNotificationId = "fxmonitor"; +const kRemoteSettingsKey = "fxmonitor-breaches"; + +async function fxmonitorNotificationShown() { + await TestUtils.waitForCondition(() => { + return ( + PopupNotifications.getNotification(kNotificationId) && + PopupNotifications.panel.state == "open" + ); + }, "Waiting for fxmonitor notification to be shown"); + ok(true, "Firefox Monitor PopupNotification was added."); +} + +async function fxmonitorNotificationGone() { + await TestUtils.waitForCondition(() => { + return ( + !PopupNotifications.getNotification(kNotificationId) && + PopupNotifications.panel.state == "closed" + ); + }, "Waiting for fxmonitor notification to go away"); + ok(true, "Firefox Monitor PopupNotification was removed."); +} + +let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 +); + +async function clearWarnedHosts() { + return new Promise((resolve, reject) => { + cps2.removeByName( + "extensions.fxmonitor.hostAlreadyWarned", + Cu.createLoadContext(), + { + handleCompletion: resolve, + } + ); + }); +} + +add_task(async function test_main_flow() { + info("Test that we show the first alert correctly for a recent breach."); + + // Pre-populate the Remote Settings collection with a breach. + let db = await RemoteSettings(kRemoteSettingsKey).db; + let BreachDate = new Date(); + let AddedDate = new Date(); + await db.create({ + Domain: "example.com", + Name: "Example Site", + BreachDate: `${BreachDate.getFullYear()}-${BreachDate.getMonth() + + 1}-${BreachDate.getDate()}`, + AddedDate: `${AddedDate.getFullYear()}-${AddedDate.getMonth() + + 1}-${AddedDate.getDate()}`, + PwnCount: 1000000, + }); + await db.importChanges({}, 1234567); + + // Trigger a sync. + await RemoteSettings(kRemoteSettingsKey).emit("sync", { + data: { + current: await RemoteSettings(kRemoteSettingsKey).get(), + }, + }); + + // Enable the extension. + await SpecialPowers.pushPrefEnv({ + set: [["extensions.fxmonitor.FirefoxMonitorURL", "http://example.org"]], + }); + + // Open a tab and wait for the alert. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com" + ); + await fxmonitorNotificationShown(); + + // Test that dismissing works. + let notification = Array.prototype.find.call( + PopupNotifications.panel.children, + elt => elt.getAttribute("popupid") == kNotificationId + ); + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + await fxmonitorNotificationGone(); + + // Reload and make sure the alert isn't shown again. + let promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + await promise; + await fxmonitorNotificationGone(); + + // Reset state. + await db.clear(); + await db.importChanges({}, 1234567); + await clearWarnedHosts(); + await SpecialPowers.pushPrefEnv({ + clear: [["extensions.fxmonitor.firstAlertShown"]], + }); + + // Reload and wait for the alert. + promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + await promise; + await fxmonitorNotificationShown(); + + // Test that the primary button opens Firefox Monitor in a new tab. + notification = Array.prototype.find.call( + PopupNotifications.panel.children, + elt => elt.getAttribute("popupid") == kNotificationId + ); + let url = `http://example.org/?breach=${encodeURIComponent( + "Example Site" + )}&utm_source=firefox&utm_medium=popup`; + promise = BrowserTestUtils.waitForNewTab(gBrowser, url); + EventUtils.synthesizeMouseAtCenter(notification.button, {}); + let newtab = await promise; + + // Close the new tab and check that the alert is gone. + BrowserTestUtils.removeTab(newtab); + await fxmonitorNotificationGone(); + + // Reset state (but not firstAlertShown). + await db.clear(); + await db.importChanges({}, 1234567); + await clearWarnedHosts(); + + info( + "Test that we do not show the second alert for a breach added over two months ago." + ); + + // Add a new "old" breach - added over 2 months ago. + AddedDate.setMonth(AddedDate.getMonth() - 3); + await db.create({ + Domain: "example.com", + Name: "Example Site", + BreachDate: `${BreachDate.getFullYear()}-${BreachDate.getMonth() + + 1}-${BreachDate.getDate()}`, + AddedDate: `${AddedDate.getFullYear()}-${AddedDate.getMonth() + + 1}-${AddedDate.getDate()}`, + PwnCount: 1000000, + }); + await db.importChanges({}, 1234567); + + // Trigger a sync. + await RemoteSettings(kRemoteSettingsKey).emit("sync", { + data: { + current: await RemoteSettings(kRemoteSettingsKey).get(), + }, + }); + + // Check that there's no alert for the old breach. + promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + await promise; + await fxmonitorNotificationGone(); + + // Reset state (but not firstAlertShown). + AddedDate.setMonth(AddedDate.getMonth() + 3); + await db.clear(); + await db.importChanges({}, 1234567); + await clearWarnedHosts(); + + info("Test that we do show the second alert for a recent breach."); + + // Add a new "recent" breach. + await db.create({ + Domain: "example.com", + Name: "Example Site", + BreachDate: `${BreachDate.getFullYear()}-${BreachDate.getMonth() + + 1}-${BreachDate.getDate()}`, + AddedDate: `${AddedDate.getFullYear()}-${AddedDate.getMonth() + + 1}-${AddedDate.getDate()}`, + PwnCount: 1000000, + }); + await db.importChanges({}, 1234567); + + // Trigger a sync. + await RemoteSettings(kRemoteSettingsKey).emit("sync", { + data: { + current: await RemoteSettings(kRemoteSettingsKey).get(), + }, + }); + + // Check that there's an alert for the new breach. + promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + await promise; + await fxmonitorNotificationShown(); + + // Reset state (including firstAlertShown) + await db.clear(); + await db.importChanges({}, 1234567); + await clearWarnedHosts(); + await SpecialPowers.pushPrefEnv({ + clear: [["extensions.fxmonitor.firstAlertShown"]], + }); + + info( + "Test that we do not show the first alert for a breach added over a year ago." + ); + + // Add a new "old" breach - added over a year ago. + AddedDate.setFullYear(AddedDate.getFullYear() - 2); + await db.create({ + Domain: "example.com", + Name: "Example Site", + BreachDate: `${BreachDate.getFullYear()}-${BreachDate.getMonth() + + 1}-${BreachDate.getDate()}`, + AddedDate: `${AddedDate.getFullYear()}-${AddedDate.getMonth() + + 1}-${AddedDate.getDate()}`, + PwnCount: 1000000, + }); + await db.importChanges({}, 1234567); + + // Trigger a sync. + await RemoteSettings(kRemoteSettingsKey).emit("sync", { + data: { + current: await RemoteSettings(kRemoteSettingsKey).get(), + }, + }); + + // Check that there's no alert for the old breach. + promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + await promise; + await fxmonitorNotificationGone(); + + // Clean up. + BrowserTestUtils.removeTab(tab); + await db.clear(); + await db.importChanges({}, 1234567); + // Trigger a sync to clear. + await RemoteSettings(kRemoteSettingsKey).emit("sync", { + data: { + current: [], + }, + }); + await clearWarnedHosts(); + await SpecialPowers.pushPrefEnv({ + clear: [["extensions.fxmonitor.firstAlertShown"]], + }); +}); |