Backed out changeset 5669c20b0617 (bug 1903669) for causing crashtest failures on...
[gecko.git] / browser / actors / AboutProtectionsParent.sys.mjs
blob1c647561ec8e56d59047c602a745f90cfa592b83
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";
7 const lazy = {};
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",
18 });
20 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
21   return ChromeUtils.importESModule(
22     "resource://gre/modules/FxAccounts.sys.mjs"
23   ).getFxAccountsSingleton();
24 });
26 XPCOMUtils.defineLazyServiceGetter(
27   lazy,
28   "TrackingDBService",
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
39   // the number.
40   [Ci.nsITrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID, "fingerprinter"],
41   [Ci.nsITrackingDBService.SOCIAL_ID, "social"],
42 ]);
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 = [
51   "profile:uid",
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"
65 // Error messages
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 = [
74   "monitoredEmails",
75   "numBreaches",
76   "passwords",
77   "numBreachesResolved",
78   "passwordsResolved",
81 let gTestOverride = null;
82 let monitorResponse = null;
83 let entrypoint = "direct";
85 export class AboutProtectionsParent extends JSWindowActorParent {
86   constructor() {
87     super();
88   }
90   // Some tests wish to override certain functions with ones that mostly do nothing.
91   static setTestOverride(callback) {
92     gTestOverride = callback;
93   }
95   /**
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.
98    *
99    * @return valid data from endpoint.
100    */
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;
107       } else {
108         return monitorResponse;
109       }
110     }
112     // Make the request
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);
118     if (response.ok) {
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.
123       let isValid = null;
124       for (let prop in json) {
125         isValid = MONITOR_RESPONSE_PROPS.includes(prop);
127         if (!isValid) {
128           break;
129         }
130       }
132       monitorResponse = isValid ? json : new Error(UNEXPECTED_RESPONSE);
133       if (isValid) {
134         monitorResponse.timestamp = Date.now();
135       }
136     } else {
137       // Check the reason for the error
138       switch (response.status) {
139         case 400:
140         case 401:
141           monitorResponse = new Error(INVALID_OAUTH_TOKEN);
142           break;
143         case 404:
144           monitorResponse = new Error(USER_UNSUBSCRIBED_TO_MONITOR);
145           break;
146         case 503:
147           monitorResponse = new Error(SERVICE_UNAVAILABLE);
148           break;
149         default:
150           monitorResponse = new Error(UNKNOWN_ERROR);
151           break;
152       }
153     }
155     if (monitorResponse instanceof Error) {
156       throw monitorResponse;
157     }
158     return monitorResponse;
159   }
161   /**
162    * Retrieves login data for the user.
163    *
164    * @return {{
165    *            numLogins: Number,
166    *            potentiallyBreachedLogins: Number,
167    *            mobileDeviceConnected: Boolean }}
168    */
169   async getLoginData() {
170     if (gTestOverride && "getLoginData" in gTestOverride) {
171       return gTestOverride.getLoginData();
172     }
174     try {
175       if (await lazy.fxAccounts.getSignedInUser()) {
176         await lazy.fxAccounts.device.refreshDeviceList();
177       }
178     } catch (e) {
179       console.error("There was an error fetching login data: ", e.message);
180     }
182     const userFacingLogins =
183       Services.logins.countLogins("", "", "") -
184       Services.logins.countLogins(
185         lazy.FXA_PWDMGR_HOST,
186         null,
187         lazy.FXA_PWDMGR_REALM
188       );
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);
197     }
199     let mobileDeviceConnected =
200       lazy.fxAccounts.device.recentDeviceList &&
201       lazy.fxAccounts.device.recentDeviceList.filter(
202         device => device.type == "mobile"
203       ).length;
205     return {
206       numLogins: userFacingLogins,
207       potentiallyBreachedLogins: potentiallyBreachedLogins
208         ? potentiallyBreachedLogins.size
209         : 0,
210       mobileDeviceConnected,
211     };
212   }
214   /**
215    * Retrieves monitor data for the user.
216    *
217    * @return {{ monitoredEmails: Number,
218    *            numBreaches: Number,
219    *            passwords: Number,
220    *            userEmail: String|null,
221    *            error: Boolean }}
222    *         Monitor data.
223    */
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;
231     }
233     let monitorData = {};
234     let userEmail = null;
235     let token = await this.getMonitorScopedOAuthToken();
237     try {
238       if (token) {
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();
244         userEmail = email;
245       } else {
246         // If no account exists, then the user is not logged in with an fxAccount.
247         monitorData = {
248           errorMessage: "No account",
249         };
250       }
251     } catch (e) {
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();
262         try {
263           monitorData = await this.fetchUserBreachStats(token);
264         } catch (_) {
265           console.error(e.message);
266         }
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();
271         userEmail = email;
272       } else {
273         monitorData.errorMessage = e.message || "An error ocurred.";
274       }
275     }
277     return {
278       ...monitorData,
279       userEmail,
280       error: !!monitorData.errorMessage,
281     };
282   }
284   async getMonitorScopedOAuthToken() {
285     let token = null;
287     try {
288       token = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_MONITOR });
289     } catch (e) {
290       console.error(
291         "There was an error fetching the user's token: ",
292         e.message
293       );
294     }
296     return token;
297   }
299   /**
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.
302    */
303   async shouldShowProxyCard() {
304     const region = lazy.Region.home || "";
305     const languages = Services.prefs.getComplexValue(
306       "intl.accept_languages",
307       Ci.nsIPrefLocalizedString
308     );
309     const alreadyInstalled = await lazy.AddonManager.getAddonByID(
310       SECURE_PROXY_ADDON_ID
311     );
313     return (
314       region.toLowerCase() === "us" &&
315       !alreadyInstalled &&
316       languages.data.toLowerCase().includes("en-us")
317     );
318   }
320   async VPNSubStatus() {
321     // For testing, set vpn sub status manually
322     if (gTestOverride && "vpnOverrides" in gTestOverride) {
323       return gTestOverride.vpnOverrides();
324     }
326     let vpnToken;
327     try {
328       vpnToken = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_VPN });
329     } catch (e) {
330       console.error(
331         "There was an error fetching the user's token: ",
332         e.message
333       );
334       // there was an error, assume user is not subscribed to VPN
335       return false;
336     }
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);
341     if (res.ok) {
342       const result = await res.json();
343       for (let sub of result) {
344         if (sub.subscriptionId == VPN_SUB_ID) {
345           return true;
346         }
347       }
348       return false;
349     }
350     // unknown logic: assume user is not subscribed to VPN
351     return false;
352   }
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",
360         });
361         break;
362       case "OpenContentBlockingPreferences":
363         win.openPreferences("privacy-trackingprotection", {
364           origin: "about-protections",
365         });
366         break;
367       case "OpenSyncPreferences":
368         win.openTrustedLinkIn("about:preferences#sync", "tab");
369         break;
370       case "FetchContentBlockingEvents":
371         let dataToSend = {};
372         let displayNames = new Services.intl.DisplayNames(undefined, {
373           type: "weekday",
374           style: "abbreviated",
375           calendar: "gregory",
376         });
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;
384           return dataToSend;
385         }
386         let sumEvents = await lazy.TrackingDBService.sumAllEvents();
387         let earliestDate =
388           await lazy.TrackingDBService.getEarliestRecordedDate();
389         let eventsByDate = await lazy.TrackingDBService.getEventsByDateRange(
390           aMessage.data.from,
391           aMessage.data.to
392         );
393         let largest = 0;
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;
402           currentCnt += count;
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;
409           }
410         }
411         dataToSend.largest = largest;
412         dataToSend.earliestDate = earliestDate;
413         dataToSend.sumEvents = sumEvents;
415         return dataToSend;
417       case "FetchMonitorData":
418         return this.getMonitorData();
420       case "FetchUserLoginsData":
421         return this.getLoginData();
423       case "ClearMonitorCache":
424         monitorResponse = null;
425         break;
427       case "GetShowProxyCard":
428         let card = await this.shouldShowProxyCard();
429         return card;
431       case "RecordEntryPoint":
432         entrypoint = aMessage.data.entrypoint;
433         break;
435       case "FetchEntryPoint":
436         return entrypoint;
438       case "FetchVPNSubStatus":
439         return this.VPNSubStatus();
441       case "FetchShowVPNCard":
442         return lazy.BrowserUtils.shouldShowVPNPromo();
443     }
445     return undefined;
446   }