From ab6e6b7baa4f6f0a6679d5bfaf75bf4bce19713f Mon Sep 17 00:00:00 2001 From: Nihanth Subramanya Date: Fri, 8 Feb 2019 08:55:59 +0000 Subject: [PATCH] Bug 1525519 - Land Firefox Monitor system add-on into browser/extensions. r=johannh Differential Revision: https://phabricator.services.mozilla.com/D18996 --HG-- extra : moz-landing-system : lando --- browser/extensions/fxmonitor/assets/alert.svg | 6 + .../extensions/fxmonitor/assets/monitor32.svg | 6 + browser/extensions/fxmonitor/background.js | 9 + .../fxmonitor/locale/en-US/strings.properties | 45 ++ browser/extensions/fxmonitor/manifest.json | 24 + browser/extensions/fxmonitor/moz.build | 38 ++ .../fxmonitor/privileged/FirefoxMonitor.css | 90 ++++ .../fxmonitor/privileged/FirefoxMonitor.jsm | 415 ++++++++++++++++++ .../extensions/fxmonitor/privileged/api.js | 32 ++ .../fxmonitor/privileged/schema.json | 13 + .../privileged/subscripts/EveryWindow.jsm | 62 +++ .../privileged/subscripts/Globals.jsm | 16 + .../privileged/subscripts/PanelUI.jsm | 117 +++++ browser/extensions/moz.build | 1 + 14 files changed, 874 insertions(+) create mode 100644 browser/extensions/fxmonitor/assets/alert.svg create mode 100644 browser/extensions/fxmonitor/assets/monitor32.svg create mode 100644 browser/extensions/fxmonitor/background.js create mode 100644 browser/extensions/fxmonitor/locale/en-US/strings.properties create mode 100644 browser/extensions/fxmonitor/manifest.json create mode 100644 browser/extensions/fxmonitor/moz.build create mode 100644 browser/extensions/fxmonitor/privileged/FirefoxMonitor.css create mode 100644 browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm create mode 100644 browser/extensions/fxmonitor/privileged/api.js create mode 100644 browser/extensions/fxmonitor/privileged/schema.json create mode 100644 browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm create mode 100644 browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm create mode 100644 browser/extensions/fxmonitor/privileged/subscripts/PanelUI.jsm diff --git a/browser/extensions/fxmonitor/assets/alert.svg b/browser/extensions/fxmonitor/assets/alert.svg new file mode 100644 index 000000000000..778cea3c7f50 --- /dev/null +++ b/browser/extensions/fxmonitor/assets/alert.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/extensions/fxmonitor/assets/monitor32.svg b/browser/extensions/fxmonitor/assets/monitor32.svg new file mode 100644 index 000000000000..c0ecd93c3214 --- /dev/null +++ b/browser/extensions/fxmonitor/assets/monitor32.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/extensions/fxmonitor/background.js b/browser/extensions/fxmonitor/background.js new file mode 100644 index 000000000000..bc3aef3cc816 --- /dev/null +++ b/browser/extensions/fxmonitor/background.js @@ -0,0 +1,9 @@ +/* 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/. */ + +/* eslint-env webextensions */ + +"use strict"; + +browser.fxmonitor.start(); diff --git a/browser/extensions/fxmonitor/locale/en-US/strings.properties b/browser/extensions/fxmonitor/locale/en-US/strings.properties new file mode 100644 index 000000000000..b4f0f228a69f --- /dev/null +++ b/browser/extensions/fxmonitor/locale/en-US/strings.properties @@ -0,0 +1,45 @@ +# 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/. + +# Header of the popup +fxmonitor.popupHeader=Have an account on this site? +# Firefox Monitor must be treated as a brand, and kept in English. +# It cannot be: +# - Declined to adapt to grammatical case. +# - Transliterated. +# - Translated. +fxmonitor.brandName=Firefox Monitor +# Tooltip text for the popup's anchor icon in the URL bar +# %S is replaced with fxmonitor.brandName. +fxmonitor.anchorIcon.tooltiptext=Site reported to %S +# Text content of popup. Semi-colon list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# This version is only used when the number of accounts is smaller than 100,000. +# The placeholders are: +# #1: The exact number of accounts compromised in the breach. +# #2: The name of the breached site. +# #3: The year of the breach. +# #4: The brand name ("Firefox Monitor"). +fxmonitor.popupText=#1 account from #2 was compromised in #3. Check #4 to see if yours is at risk.;#1 accounts from #2 were compromised in #3. Check #4 to see if yours is at risk. +# Text content of popup. Semi-colon list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# This version is only used when the number of accounts is greater than 100,000. +# The placeholders are: +# #1: The number of accounts compromised in the breach, rounded down to the +# most significant digit. +# Ex.: 234,567 -> More than 200,000 accounts [...] +# 345,678,901 -> More than 300,000,000 accounts [...] +# 4,567,890,123 -> More than 4,000,000,000 accounts [...] +# #2: The name of the breached site. +# #3: The year of the breach. +# #4: The brand name ("Firefox Monitor"). +fxmonitor.popupTextRounded=More than #1 account from #2 was compromised in #3. Check #4 to see if yours is at risk.;More than #1 accounts from #2 were compromised in #3. Check #4 to see if yours is at risk. +# %S is replaced with fxmonitor.brandName. +fxmonitor.checkButton.label=Check %S +fxmonitor.checkButton.accessKey=C +fxmonitor.dismissButton.label=Dismiss +fxmonitor.dismissButton.accessKey=D +# %S is replaced with fxmonitor.brandName. +fxmonitor.neverShowButton.label=Never show %S alerts +fxmonitor.neverShowButton.accessKey=N diff --git a/browser/extensions/fxmonitor/manifest.json b/browser/extensions/fxmonitor/manifest.json new file mode 100644 index 000000000000..316189ce35e9 --- /dev/null +++ b/browser/extensions/fxmonitor/manifest.json @@ -0,0 +1,24 @@ +{ + "manifest_version": 2, + "name": "Firefox Monitor", + "version": "3.0", + "applications": { + "gecko": { + "id": "fxmonitor@mozilla.org", + "strict_min_version": "65.0" + } + }, + "background": { + "scripts": ["background.js"] + }, + "experiment_apis": { + "fxmonitor": { + "schema": "./privileged/schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "./privileged/api.js", + "paths": [["fxmonitor"]] + } + } + } +} diff --git a/browser/extensions/fxmonitor/moz.build b/browser/extensions/fxmonitor/moz.build new file mode 100644 index 000000000000..330aaa070de4 --- /dev/null +++ b/browser/extensions/fxmonitor/moz.build @@ -0,0 +1,38 @@ +# -*- 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/. + +DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION'] +DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION'] + +FINAL_TARGET_FILES.features['fxmonitor@mozilla.org'] += [ + 'background.js', + 'manifest.json' +] + +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' +] + +FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['privileged']['subscripts'] += [ + 'privileged/subscripts/EveryWindow.jsm', + 'privileged/subscripts/Globals.jsm', + 'privileged/subscripts/PanelUI.jsm' +] + +FINAL_TARGET_FILES.features['fxmonitor@mozilla.org']['locale']['en-US'] += [ + 'locale/en-US/strings.properties' +] + +with Files('**'): + BUG_COMPONENT = ('Firefox', 'Firefox Monitor') diff --git a/browser/extensions/fxmonitor/privileged/FirefoxMonitor.css b/browser/extensions/fxmonitor/privileged/FirefoxMonitor.css new file mode 100644 index 000000000000..69b697578194 --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/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/extensions/fxmonitor/privileged/FirefoxMonitor.jsm b/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm new file mode 100644 index 000000000000..abf7eddcca6e --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/FirefoxMonitor.jsm @@ -0,0 +1,415 @@ +/* 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 */ + +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, + + // loadStrings loads a stringbundle into this property. + 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", + + 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, false, + (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; + } + + /* globals Preferences, RemoteSettings, fetch, btoa, XUL_NS */ + Services.scriptloader.loadSubScript( + this.getURL("privileged/subscripts/Globals.jsm")); + + /* globals EveryWindow */ + Services.scriptloader.loadSubScript( + this.getURL("privileged/subscripts/EveryWindow.jsm")); + + /* globals PanelUI */ + Services.scriptloader.loadSubScript( + this.getURL("privileged/subscripts/PanelUI.jsm")); + + Services.telemetry.registerEvents("fxmonitor", { + "interaction": { + methods: ["interaction"], + objects: [ + "doorhanger_shown", + "doorhanger_removed", + "check_btn", + "dismiss_btn", + "never_show_btn", + ], + // Disabled for now, pending data review (bug 1525977) + record_on_release: false, + }, + }); + + // Disabled for now, pending data review (bug 1525977) + Services.telemetry.setEventRecordingEnabled("fxmonitor", false); + + 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(); + + this._delayedInited = true; + }, + + async loadStrings() { + // Services.strings.createBundle has a whitelist of URL schemes that it + // accepts. moz-extension: is not one of them, so we work around that + // by reading the file manually and creating a data: URL (allowed). + let response; + let locale = Services.locale.defaultLocale; + try { + response = await fetch(this.getURL(`locale/${locale}/strings.properties`)); + } catch (e) { + Cu.reportError(`Firefox Monitor: no strings available for ${locale}. Falling back to en-US.`); + response = await fetch(this.getURL(`locale/en-US/strings.properties`)); + } + let buffer = await response.arrayBuffer(); + let binary = ""; + let bytes = new Uint8Array(buffer); + let len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + let b64 = btoa(binary); + this.strings = Services.strings.createBundle(`data:text/plain;base64,${b64}`); + }, + + 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); + }, + + async startObserving() { + if (this.observerAdded) { + return; + } + + await this.delayedInit(); + + EveryWindow.registerCallback( + this.kNotificationID, + (win) => { + // Inject our stylesheet. + let DOMWindowUtils = win.windowUtils; + DOMWindowUtils.loadSheetUsingURIString(this.getURL("privileged/FirefoxMonitor.css"), + DOMWindowUtils.AUTHOR_SHEET); + + // Set up some helper functions on the window object + // for the popup notification to use. + win.FirefoxMonitorUtils = { + // Keeps track of all notifications currently shown, + // so that we can clear them out properly if we get + // disabled. + notifications: new Set(), + disable: () => { + this.disable(); + }, + getString: (aKey) => { + return this.getString(aKey); + }, + getFormattedString: (aKey, args) => { + return this.getFormattedString(aKey, args); + }, + getFirefoxMonitorURL: (aSiteName) => { + return `${this.FirefoxMonitorURL}/?breach=${encodeURIComponent(aSiteName)}&utm_source=firefox&utm_medium=popup`; + }, + }; + + // 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); + win.FirefoxMonitorPanelUI = panelUI; + + // Start listening across all tabs! + win.gBrowser.addTabsProgressListener(this); + }, + (win) => { + // If the window is being destroyed and gBrowser no longer exists, + // don't bother doing anything. + if (!win.gBrowser) { + return; + } + + let DOMWindowUtils = win.windowUtils; + if (!DOMWindowUtils) { + // win.windowUtils was added in 63, fallback if it's not available. + DOMWindowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + } + DOMWindowUtils.removeSheetUsingURIString(this.getURL("privileged/FirefoxMonitor.css"), + DOMWindowUtils.AUTHOR_SHEET); + + win.FirefoxMonitorUtils.notifications.forEach(n => { + n.remove(); + }); + delete win.FirefoxMonitorUtils; + + let doc = win.document; + doc.getElementById(`${this.kNotificationID}-notification-anchor`).remove(); + doc.getElementById(`${this.kNotificationID}-notification`).remove(); + delete win.FirefoxMonitorPanelUI; + + win.gBrowser.removeTabsProgressListener(this); + }, + ); + + this.observerAdded = true; + }, + + 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 = doc.defaultView.FirefoxMonitorPanelUI; + + 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": + win.FirefoxMonitorUtils.notifications.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"); + + win.FirefoxMonitorUtils.notifications.add(n); + }, +}; diff --git a/browser/extensions/fxmonitor/privileged/api.js b/browser/extensions/fxmonitor/privileged/api.js new file mode 100644 index 000000000000..b4a21a5639d0 --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/api.js @@ -0,0 +1,32 @@ +/* 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 */ + +ChromeUtils.defineModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +let FirefoxMonitorContainer = {}; + +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); + }, + }, + }; + } + + onShutdown(shutdownReason) { + if (Services.startup.shuttingDown || !FirefoxMonitorContainer.FirefoxMonitor) { + return; + } + + FirefoxMonitorContainer.FirefoxMonitor.stopObserving(); + } +}; diff --git a/browser/extensions/fxmonitor/privileged/schema.json b/browser/extensions/fxmonitor/privileged/schema.json new file mode 100644 index 000000000000..c5edd97db9ef --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/schema.json @@ -0,0 +1,13 @@ +[ + { + "namespace": "fxmonitor", + "functions": [ + { + "name": "start", + "type": "function", + "async": true, + "parameters": [] + } + ] + } +] diff --git a/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm b/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm new file mode 100644 index 000000000000..0a631bb7fc98 --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/subscripts/EveryWindow.jsm @@ -0,0 +1,62 @@ +/* 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 */ + +this.EveryWindow = { + _callbacks: new Map(), + _initialized: false, + + registerCallback: function EW_registerCallback(id, init, uninit) { + if (this._callbacks.has(id)) { + return; + } + + this._callForEveryWindow(init); + this._callbacks.set(id, {id, init, uninit}); + + if (!this._initialized) { + Services.obs.addObserver(this._onOpenWindow.bind(this), + "browser-delayed-startup-finished"); + this._initialized = true; + } + }, + + unregisterCallback: function EW_unregisterCallback(aId, aCallUninit = true) { + if (!this._callbacks.has(aId)) { + return; + } + + if (aCallUninit) { + this._callForEveryWindow(this._callbacks.get(aId).uninit); + } + + this._callbacks.delete(aId); + }, + + _callForEveryWindow(aFunction) { + let windowList = Services.wm.getEnumerator("navigator:browser"); + while (windowList.hasMoreElements()) { + let win = windowList.getNext(); + win.delayedStartupPromise.then(() => { aFunction(win); }); + } + }, + + _onOpenWindow(aWindow) { + for (let c of this._callbacks.values()) { + c.init(aWindow); + } + + aWindow.addEventListener("unload", + this._onWindowClosing.bind(this), + { once: true }); + }, + + _onWindowClosing(aEvent) { + let win = aEvent.target; + for (let c of this._callbacks.values()) { + c.uninit(win); + } + }, +}; diff --git a/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm b/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm new file mode 100644 index 000000000000..709ae38aaa5d --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/subscripts/Globals.jsm @@ -0,0 +1,16 @@ +/* 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/. */ + +/* eslint-disable no-unused-vars */ + +ChromeUtils.defineModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); +ChromeUtils.defineModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); +ChromeUtils.defineModuleGetter(this, "RemoteSettings", + "resource://services-settings/remote-settings.js"); +const {setTimeout, clearTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm", {}); +Cu.importGlobalProperties(["fetch", "btoa"]); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; diff --git a/browser/extensions/fxmonitor/privileged/subscripts/PanelUI.jsm b/browser/extensions/fxmonitor/privileged/subscripts/PanelUI.jsm new file mode 100644 index 000000000000..e941eaf9aa1f --- /dev/null +++ b/browser/extensions/fxmonitor/privileged/subscripts/PanelUI.jsm @@ -0,0 +1,117 @@ +/* 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 XUL_NS, Services, PluralForm */ + +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 = { + get FirefoxMonitorUtils() { + // Set on every window by FirefoxMonitor.jsm for PanelUI to use. + // Because sharing is caring. + return this.doc.defaultView.FirefoxMonitorUtils; + }, + + getString(aKey) { + return this.FirefoxMonitorUtils.getString(aKey); + }, + + getFormattedString(aKey, args) { + return this.FirefoxMonitorUtils.getFormattedString(aKey, args); + }, + + get brandString() { + if (this._brandString) { + return this._brandString; + } + return this._brandString = this.getString("fxmonitor.brandName"); + }, + + 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( + win.FirefoxMonitorUtils.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: () => { + this.FirefoxMonitorUtils.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); + }, +}; diff --git a/browser/extensions/moz.build b/browser/extensions/moz.build index 0a83bd74f3aa..e2cb169fd157 100644 --- a/browser/extensions/moz.build +++ b/browser/extensions/moz.build @@ -6,6 +6,7 @@ DIRS += [ 'formautofill', + 'fxmonitor', 'pdfjs', 'screenshots', 'webcompat',