1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
11 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
12 FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.sys.mjs",
13 FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.sys.mjs",
14 LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
15 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
17 Region: "resource://gre/modules/Region.sys.mjs",
20 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
21 return ChromeUtils.importESModule(
22 "resource://gre/modules/FxAccounts.sys.mjs"
23 ).getFxAccountsSingleton();
26 XPCOMUtils.defineLazyServiceGetter(
29 "@mozilla.org/tracking-db-service;1",
30 "nsITrackingDBService"
33 let idToTextMap = new Map([
34 [Ci.nsITrackingDBService.TRACKERS_ID, "tracker"],
35 [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookie"],
36 [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominer"],
37 [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinter"],
38 // We map the suspicious fingerprinter to fingerprinter category to aggregate
40 [Ci.nsITrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID, "fingerprinter"],
41 [Ci.nsITrackingDBService.SOCIAL_ID, "social"],
44 const MONITOR_API_ENDPOINT = Services.urlFormatter.formatURLPref(
45 "browser.contentblocking.report.endpoint_url"
48 const SECURE_PROXY_ADDON_ID = "secure-proxy@mozilla.com";
50 const SCOPE_MONITOR = [
52 "https://identity.mozilla.com/apps/monitor",
55 const SCOPE_VPN = "profile https://identity.mozilla.com/account/subscriptions";
56 const VPN_ENDPOINT = `${Services.prefs.getStringPref(
57 "identity.fxaccounts.auth.uri"
58 )}oauth/subscriptions/active`;
60 // The ID of the vpn subscription, if we see this ID attached to a user's account then they have subscribed to vpn.
61 const VPN_SUB_ID = Services.prefs.getStringPref(
62 "browser.contentblocking.report.vpn_sub_id"
66 const INVALID_OAUTH_TOKEN = "Invalid OAuth token";
67 const USER_UNSUBSCRIBED_TO_MONITOR = "User is not subscribed to Monitor";
68 const SERVICE_UNAVAILABLE = "Service unavailable";
69 const UNEXPECTED_RESPONSE = "Unexpected response";
70 const UNKNOWN_ERROR = "Unknown error";
72 // Valid response info for successful Monitor data
73 const MONITOR_RESPONSE_PROPS = [
77 "numBreachesResolved",
81 let gTestOverride = null;
82 let monitorResponse = null;
83 let entrypoint = "direct";
85 export class AboutProtectionsParent extends JSWindowActorParent {
90 // Some tests wish to override certain functions with ones that mostly do nothing.
91 static setTestOverride(callback) {
92 gTestOverride = callback;
96 * Fetches and validates data from the Monitor endpoint. If successful, then return
97 * expected data. Otherwise, throw the appropriate error depending on the status code.
99 * @return valid data from endpoint.
101 async fetchUserBreachStats(token) {
102 if (monitorResponse && monitorResponse.timestamp) {
103 var timeDiff = Date.now() - monitorResponse.timestamp;
104 let oneDayInMS = 24 * 60 * 60 * 1000;
105 if (timeDiff >= oneDayInMS) {
106 monitorResponse = null;
108 return monitorResponse;
113 const headers = new Headers();
114 headers.append("Authorization", `Bearer ${token}`);
115 const request = new Request(MONITOR_API_ENDPOINT, { headers });
116 const response = await fetch(request);
119 // Validate the shape of the response is what we're expecting.
120 const json = await response.json();
122 // Make sure that we're getting the expected data.
124 for (let prop in json) {
125 isValid = MONITOR_RESPONSE_PROPS.includes(prop);
132 monitorResponse = isValid ? json : new Error(UNEXPECTED_RESPONSE);
134 monitorResponse.timestamp = Date.now();
137 // Check the reason for the error
138 switch (response.status) {
141 monitorResponse = new Error(INVALID_OAUTH_TOKEN);
144 monitorResponse = new Error(USER_UNSUBSCRIBED_TO_MONITOR);
147 monitorResponse = new Error(SERVICE_UNAVAILABLE);
150 monitorResponse = new Error(UNKNOWN_ERROR);
155 if (monitorResponse instanceof Error) {
156 throw monitorResponse;
158 return monitorResponse;
162 * Retrieves login data for the user.
166 * potentiallyBreachedLogins: Number,
167 * mobileDeviceConnected: Boolean }}
169 async getLoginData() {
170 if (gTestOverride && "getLoginData" in gTestOverride) {
171 return gTestOverride.getLoginData();
175 if (await lazy.fxAccounts.getSignedInUser()) {
176 await lazy.fxAccounts.device.refreshDeviceList();
179 console.error("There was an error fetching login data: ", e.message);
182 const userFacingLogins =
183 Services.logins.countLogins("", "", "") -
184 Services.logins.countLogins(
185 lazy.FXA_PWDMGR_HOST,
187 lazy.FXA_PWDMGR_REALM
190 let potentiallyBreachedLogins = null;
191 // Get the stats for number of potentially breached Lockwise passwords
192 // if the Primary Password isn't locked.
193 if (userFacingLogins && Services.logins.isLoggedIn) {
194 const logins = await lazy.LoginHelper.getAllUserFacingLogins();
195 potentiallyBreachedLogins =
196 await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins);
199 let mobileDeviceConnected =
200 lazy.fxAccounts.device.recentDeviceList &&
201 lazy.fxAccounts.device.recentDeviceList.filter(
202 device => device.type == "mobile"
206 numLogins: userFacingLogins,
207 potentiallyBreachedLogins: potentiallyBreachedLogins
208 ? potentiallyBreachedLogins.size
210 mobileDeviceConnected,
215 * Retrieves monitor data for the user.
217 * @return {{ monitoredEmails: Number,
218 * numBreaches: Number,
220 * userEmail: String|null,
224 async getMonitorData() {
225 if (gTestOverride && "getMonitorData" in gTestOverride) {
226 monitorResponse = gTestOverride.getMonitorData();
227 monitorResponse.timestamp = Date.now();
228 // In a test, expect this to not fetch from the monitor endpoint due to the timestamp guaranteeing we use the cache.
229 monitorResponse = await this.fetchUserBreachStats();
230 return monitorResponse;
233 let monitorData = {};
234 let userEmail = null;
235 let token = await this.getMonitorScopedOAuthToken();
239 monitorData = await this.fetchUserBreachStats(token);
241 // Send back user's email so the protections report can direct them to the proper
242 // OAuth flow on Monitor.
243 const { email } = await lazy.fxAccounts.getSignedInUser();
246 // If no account exists, then the user is not logged in with an fxAccount.
248 errorMessage: "No account",
252 console.error(e.message);
253 monitorData.errorMessage = e.message;
255 // If the user's OAuth token is invalid, we clear the cached token and refetch
256 // again. If OAuth token is invalid after the second fetch, then the monitor UI
257 // will simply show the "no logins" UI version.
258 if (e.message === INVALID_OAUTH_TOKEN) {
259 await lazy.fxAccounts.removeCachedOAuthToken({ token });
260 token = await this.getMonitorScopedOAuthToken();
263 monitorData = await this.fetchUserBreachStats(token);
265 console.error(e.message);
267 } else if (e.message === USER_UNSUBSCRIBED_TO_MONITOR) {
268 // Send back user's email so the protections report can direct them to the proper
269 // OAuth flow on Monitor.
270 const { email } = await lazy.fxAccounts.getSignedInUser();
273 monitorData.errorMessage = e.message || "An error ocurred.";
280 error: !!monitorData.errorMessage,
284 async getMonitorScopedOAuthToken() {
288 token = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_MONITOR });
291 "There was an error fetching the user's token: ",
300 * The proxy card will only show if the user is in the US, has the browser language in "en-US",
301 * and does not yet have Proxy installed.
303 async shouldShowProxyCard() {
304 const region = lazy.Region.home || "";
305 const languages = Services.prefs.getComplexValue(
306 "intl.accept_languages",
307 Ci.nsIPrefLocalizedString
309 const alreadyInstalled = await lazy.AddonManager.getAddonByID(
310 SECURE_PROXY_ADDON_ID
314 region.toLowerCase() === "us" &&
316 languages.data.toLowerCase().includes("en-us")
320 async VPNSubStatus() {
321 // For testing, set vpn sub status manually
322 if (gTestOverride && "vpnOverrides" in gTestOverride) {
323 return gTestOverride.vpnOverrides();
328 vpnToken = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_VPN });
331 "There was an error fetching the user's token: ",
334 // there was an error, assume user is not subscribed to VPN
337 let headers = new Headers();
338 headers.append("Authorization", `Bearer ${vpnToken}`);
339 const request = new Request(VPN_ENDPOINT, { headers });
340 const res = await fetch(request);
342 const result = await res.json();
343 for (let sub of result) {
344 if (sub.subscriptionId == VPN_SUB_ID) {
350 // unknown logic: assume user is not subscribed to VPN
354 async receiveMessage(aMessage) {
355 let win = this.browsingContext.top.embedderElement.ownerGlobal;
356 switch (aMessage.name) {
357 case "OpenAboutLogins":
358 lazy.LoginHelper.openPasswordManager(win, {
359 entryPoint: "aboutprotections",
362 case "OpenContentBlockingPreferences":
363 win.openPreferences("privacy-trackingprotection", {
364 origin: "about-protections",
367 case "OpenSyncPreferences":
368 win.openTrustedLinkIn("about:preferences#sync", "tab");
370 case "FetchContentBlockingEvents":
372 let displayNames = new Services.intl.DisplayNames(undefined, {
374 style: "abbreviated",
378 // Weekdays starting Sunday (7) to Saturday (6).
379 let weekdays = [7, 1, 2, 3, 4, 5, 6].map(day => displayNames.of(day));
380 dataToSend.weekdays = weekdays;
382 if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
383 dataToSend.isPrivate = true;
386 let sumEvents = await lazy.TrackingDBService.sumAllEvents();
388 await lazy.TrackingDBService.getEarliestRecordedDate();
389 let eventsByDate = await lazy.TrackingDBService.getEventsByDateRange(
395 for (let result of eventsByDate) {
396 let count = result.getResultByName("count");
397 let type = result.getResultByName("type");
398 let timestamp = result.getResultByName("timestamp");
399 let typeStr = idToTextMap.get(type);
400 dataToSend[timestamp] = dataToSend[timestamp] ?? { total: 0 };
401 let currentCnt = dataToSend[timestamp][typeStr] ?? 0;
403 dataToSend[timestamp][typeStr] = currentCnt;
404 dataToSend[timestamp].total += count;
405 // Record the largest amount of tracking events found per day,
406 // to create the tallest column on the graph and compare other days to.
407 if (largest < dataToSend[timestamp].total) {
408 largest = dataToSend[timestamp].total;
411 dataToSend.largest = largest;
412 dataToSend.earliestDate = earliestDate;
413 dataToSend.sumEvents = sumEvents;
417 case "FetchMonitorData":
418 return this.getMonitorData();
420 case "FetchUserLoginsData":
421 return this.getLoginData();
423 case "ClearMonitorCache":
424 monitorResponse = null;
427 case "GetShowProxyCard":
428 let card = await this.shouldShowProxyCard();
431 case "RecordEntryPoint":
432 entrypoint = aMessage.data.entrypoint;
435 case "FetchEntryPoint":
438 case "FetchVPNSubStatus":
439 return this.VPNSubStatus();
441 case "FetchShowVPNCard":
442 return lazy.BrowserUtils.shouldShowVPNPromo();