Repository URL to install this package:
|
Version:
69.0-1 ▾
|
PK
!<í.s+ + chrome.manifestlocale fxmonitor en-US en-US/locale/en-US/
PK
!<K-ÂU& & assets/alert.svg<!-- 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="28" height="28" xmlns="http://www.w3.org/2000/svg">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M27.386 22.024L17.48 2.212a4 4 0 0 0-7.156 0L.418 22.032a4 4 0 0 0 3.578 5.78h19.81a4 4 0 0 0 3.58-5.788zM11.902 7.812a2 2 0 1 1 4 0v8a2 2 0 1 1-4 0v-8zm2 16.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/>
</svg>
PK
!<ã¤@ @ assets/monitor32.svg<!-- 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>
PK
!<î>
background.js/* 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();
PK
!<n^ý ý ' en-US/locale/en-US/fxmonitor.properties
fxmonitor.popupHeader=Have an account on this site?
fxmonitor.brandName=Firefox Monitor
fxmonitor.anchorIcon.tooltiptext=Site reported to %S
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.
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.
fxmonitor.checkButton.label=Check %S
fxmonitor.checkButton.accessKey=C
fxmonitor.dismissButton.label=Dismiss
fxmonitor.dismissButton.accessKey=D
fxmonitor.neverShowButton.label=Never show %S alerts
fxmonitor.neverShowButton.accessKey=N
PK
!<>\ÙØä ä
manifest.json{
"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"]]
}
}
}
}
PK
!<S±Ö Ö privileged/FirefoxMonitor.css/* 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;
}
PK
!<]ËML L privileged/api.js/* 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, 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"
);
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
this.fxmonitor = class extends ExtensionAPI {
getAPI(context) {
return {
fxmonitor: {
async start() {
await FirefoxMonitor.init(context.extension);
},
},
};
}
onShutdown(shutdownReason) {
if (Services.startup.shuttingDown) {
return;
}
FirefoxMonitor.stopObserving();
}
};
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);
},
getURL(aPath) {
return this.extension.getURL(aPath);
},
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(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;
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,
},
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"FirefoxMonitorURL",
this.kFirefoxMonitorURLPref,
this.kDefaultFirefoxMonitorURL
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"firstAlertShown",
this.kFirstAlertShownPref,
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"telemetryDisabled",
this.kTelemetryDisabledPref,
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) {
XPCOMUtils.defineLazyGetter(this, "strings", () => {
Components.manager.addBootstrappedManifestLocation(l10nManifest);
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
);
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(
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;
},
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: this.getURL("assets/monitor32.svg"),
}
);
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.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",
{}
);
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);
},
};
PK
!<Oµ{¸ ¸ privileged/schema.json[
{
"namespace": "fxmonitor",
"functions": [
{
"name": "start",
"type": "function",
"async": true,
"parameters": []
}
]
}
]
PK
!<í.s+ + chrome.manifestPK
!<K-ÂU& & ¤X assets/alert.svgPK
!<ã¤@ @ ¤¬ assets/monitor32.svgPK
!<î>
¤ background.jsPK
!<n^ý ý ' a
en-US/locale/en-US/fxmonitor.propertiesPK
!<>\ÙØä ä
¤£
manifest.jsonPK
!<S±Ö Ö ¤² privileged/FirefoxMonitor.cssPK
!<]ËML L ¤Ã privileged/api.jsPK
!<Oµ{¸ ¸ ¤þc privileged/schema.jsonPK V êd