Bug 1845017 - Disable the TestPHCExhaustion test r=glandium
[gecko.git] / browser / actors / AboutProtectionsParent.sys.mjs
blob147d1ec247cf3bac955455f7b5b59da4fff1dc9b
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   [Ci.nsITrackingDBService.SOCIAL_ID, "social"],
39 ]);
41 const MONITOR_API_ENDPOINT = Services.urlFormatter.formatURLPref(
42   "browser.contentblocking.report.endpoint_url"
45 const SECURE_PROXY_ADDON_ID = "secure-proxy@mozilla.com";
47 const SCOPE_MONITOR = [
48   "profile:uid",
49   "https://identity.mozilla.com/apps/monitor",
52 const SCOPE_VPN = "profile https://identity.mozilla.com/account/subscriptions";
53 const VPN_ENDPOINT = `${Services.prefs.getStringPref(
54   "identity.fxaccounts.auth.uri"
55 )}oauth/subscriptions/active`;
57 // The ID of the vpn subscription, if we see this ID attached to a user's account then they have subscribed to vpn.
58 const VPN_SUB_ID = Services.prefs.getStringPref(
59   "browser.contentblocking.report.vpn_sub_id"
62 // Error messages
63 const INVALID_OAUTH_TOKEN = "Invalid OAuth token";
64 const USER_UNSUBSCRIBED_TO_MONITOR = "User is not subscribed to Monitor";
65 const SERVICE_UNAVAILABLE = "Service unavailable";
66 const UNEXPECTED_RESPONSE = "Unexpected response";
67 const UNKNOWN_ERROR = "Unknown error";
69 // Valid response info for successful Monitor data
70 const MONITOR_RESPONSE_PROPS = [
71   "monitoredEmails",
72   "numBreaches",
73   "passwords",
74   "numBreachesResolved",
75   "passwordsResolved",
78 let gTestOverride = null;
79 let monitorResponse = null;
80 let entrypoint = "direct";
82 export class AboutProtectionsParent extends JSWindowActorParent {
83   constructor() {
84     super();
85   }
87   // Some tests wish to override certain functions with ones that mostly do nothing.
88   static setTestOverride(callback) {
89     gTestOverride = callback;
90   }
92   /**
93    * Fetches and validates data from the Monitor endpoint. If successful, then return
94    * expected data. Otherwise, throw the appropriate error depending on the status code.
95    *
96    * @return valid data from endpoint.
97    */
98   async fetchUserBreachStats(token) {
99     if (monitorResponse && monitorResponse.timestamp) {
100       var timeDiff = Date.now() - monitorResponse.timestamp;
101       let oneDayInMS = 24 * 60 * 60 * 1000;
102       if (timeDiff >= oneDayInMS) {
103         monitorResponse = null;
104       } else {
105         return monitorResponse;
106       }
107     }
109     // Make the request
110     const headers = new Headers();
111     headers.append("Authorization", `Bearer ${token}`);
112     const request = new Request(MONITOR_API_ENDPOINT, { headers });
113     const response = await fetch(request);
115     if (response.ok) {
116       // Validate the shape of the response is what we're expecting.
117       const json = await response.json();
119       // Make sure that we're getting the expected data.
120       let isValid = null;
121       for (let prop in json) {
122         isValid = MONITOR_RESPONSE_PROPS.includes(prop);
124         if (!isValid) {
125           break;
126         }
127       }
129       monitorResponse = isValid ? json : new Error(UNEXPECTED_RESPONSE);
130       if (isValid) {
131         monitorResponse.timestamp = Date.now();
132       }
133     } else {
134       // Check the reason for the error
135       switch (response.status) {
136         case 400:
137         case 401:
138           monitorResponse = new Error(INVALID_OAUTH_TOKEN);
139           break;
140         case 404:
141           monitorResponse = new Error(USER_UNSUBSCRIBED_TO_MONITOR);
142           break;
143         case 503:
144           monitorResponse = new Error(SERVICE_UNAVAILABLE);
145           break;
146         default:
147           monitorResponse = new Error(UNKNOWN_ERROR);
148           break;
149       }
150     }
152     if (monitorResponse instanceof Error) {
153       throw monitorResponse;
154     }
155     return monitorResponse;
156   }
158   /**
159    * Retrieves login data for the user.
160    *
161    * @return {{
162    *            numLogins: Number,
163    *            potentiallyBreachedLogins: Number,
164    *            mobileDeviceConnected: Boolean }}
165    */
166   async getLoginData() {
167     if (gTestOverride && "getLoginData" in gTestOverride) {
168       return gTestOverride.getLoginData();
169     }
171     try {
172       if (await lazy.fxAccounts.getSignedInUser()) {
173         await lazy.fxAccounts.device.refreshDeviceList();
174       }
175     } catch (e) {
176       console.error("There was an error fetching login data: ", e.message);
177     }
179     const userFacingLogins =
180       Services.logins.countLogins("", "", "") -
181       Services.logins.countLogins(
182         lazy.FXA_PWDMGR_HOST,
183         null,
184         lazy.FXA_PWDMGR_REALM
185       );
187     let potentiallyBreachedLogins = null;
188     // Get the stats for number of potentially breached Lockwise passwords
189     // if the Primary Password isn't locked.
190     if (userFacingLogins && Services.logins.isLoggedIn) {
191       const logins = await lazy.LoginHelper.getAllUserFacingLogins();
192       potentiallyBreachedLogins =
193         await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins);
194     }
196     let mobileDeviceConnected =
197       lazy.fxAccounts.device.recentDeviceList &&
198       lazy.fxAccounts.device.recentDeviceList.filter(
199         device => device.type == "mobile"
200       ).length;
202     return {
203       numLogins: userFacingLogins,
204       potentiallyBreachedLogins: potentiallyBreachedLogins
205         ? potentiallyBreachedLogins.size
206         : 0,
207       mobileDeviceConnected,
208     };
209   }
211   /**
212    * Retrieves monitor data for the user.
213    *
214    * @return {{ monitoredEmails: Number,
215    *            numBreaches: Number,
216    *            passwords: Number,
217    *            userEmail: String|null,
218    *            error: Boolean }}
219    *         Monitor data.
220    */
221   async getMonitorData() {
222     if (gTestOverride && "getMonitorData" in gTestOverride) {
223       monitorResponse = gTestOverride.getMonitorData();
224       monitorResponse.timestamp = Date.now();
225       // In a test, expect this to not fetch from the monitor endpoint due to the timestamp guaranteeing we use the cache.
226       monitorResponse = await this.fetchUserBreachStats();
227       return monitorResponse;
228     }
230     let monitorData = {};
231     let userEmail = null;
232     let token = await this.getMonitorScopedOAuthToken();
234     try {
235       if (token) {
236         monitorData = await this.fetchUserBreachStats(token);
238         // Send back user's email so the protections report can direct them to the proper
239         // OAuth flow on Monitor.
240         const { email } = await lazy.fxAccounts.getSignedInUser();
241         userEmail = email;
242       } else {
243         // If no account exists, then the user is not logged in with an fxAccount.
244         monitorData = {
245           errorMessage: "No account",
246         };
247       }
248     } catch (e) {
249       console.error(e.message);
250       monitorData.errorMessage = e.message;
252       // If the user's OAuth token is invalid, we clear the cached token and refetch
253       // again. If OAuth token is invalid after the second fetch, then the monitor UI
254       // will simply show the "no logins" UI version.
255       if (e.message === INVALID_OAUTH_TOKEN) {
256         await lazy.fxAccounts.removeCachedOAuthToken({ token });
257         token = await this.getMonitorScopedOAuthToken();
259         try {
260           monitorData = await this.fetchUserBreachStats(token);
261         } catch (_) {
262           console.error(e.message);
263         }
264       } else if (e.message === USER_UNSUBSCRIBED_TO_MONITOR) {
265         // Send back user's email so the protections report can direct them to the proper
266         // OAuth flow on Monitor.
267         const { email } = await lazy.fxAccounts.getSignedInUser();
268         userEmail = email;
269       } else {
270         monitorData.errorMessage = e.message || "An error ocurred.";
271       }
272     }
274     return {
275       ...monitorData,
276       userEmail,
277       error: !!monitorData.errorMessage,
278     };
279   }
281   async getMonitorScopedOAuthToken() {
282     let token = null;
284     try {
285       token = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_MONITOR });
286     } catch (e) {
287       console.error(
288         "There was an error fetching the user's token: ",
289         e.message
290       );
291     }
293     return token;
294   }
296   /**
297    * The proxy card will only show if the user is in the US, has the browser language in "en-US",
298    * and does not yet have Proxy installed.
299    */
300   async shouldShowProxyCard() {
301     const region = lazy.Region.home || "";
302     const languages = Services.prefs.getComplexValue(
303       "intl.accept_languages",
304       Ci.nsIPrefLocalizedString
305     );
306     const alreadyInstalled = await lazy.AddonManager.getAddonByID(
307       SECURE_PROXY_ADDON_ID
308     );
310     return (
311       region.toLowerCase() === "us" &&
312       !alreadyInstalled &&
313       languages.data.toLowerCase().includes("en-us")
314     );
315   }
317   async VPNSubStatus() {
318     // For testing, set vpn sub status manually
319     if (gTestOverride && "vpnOverrides" in gTestOverride) {
320       return gTestOverride.vpnOverrides();
321     }
323     let vpnToken;
324     try {
325       vpnToken = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_VPN });
326     } catch (e) {
327       console.error(
328         "There was an error fetching the user's token: ",
329         e.message
330       );
331       // there was an error, assume user is not subscribed to VPN
332       return false;
333     }
334     let headers = new Headers();
335     headers.append("Authorization", `Bearer ${vpnToken}`);
336     const request = new Request(VPN_ENDPOINT, { headers });
337     const res = await fetch(request);
338     if (res.ok) {
339       const result = await res.json();
340       for (let sub of result) {
341         if (sub.subscriptionId == VPN_SUB_ID) {
342           return true;
343         }
344       }
345       return false;
346     }
347     // unknown logic: assume user is not subscribed to VPN
348     return false;
349   }
351   async receiveMessage(aMessage) {
352     let win = this.browsingContext.top.embedderElement.ownerGlobal;
353     switch (aMessage.name) {
354       case "OpenAboutLogins":
355         lazy.LoginHelper.openPasswordManager(win, {
356           entryPoint: "aboutprotections",
357         });
358         break;
359       case "OpenContentBlockingPreferences":
360         win.openPreferences("privacy-trackingprotection", {
361           origin: "about-protections",
362         });
363         break;
364       case "OpenSyncPreferences":
365         win.openTrustedLinkIn("about:preferences#sync", "tab");
366         break;
367       case "FetchContentBlockingEvents":
368         let dataToSend = {};
369         let displayNames = new Services.intl.DisplayNames(undefined, {
370           type: "weekday",
371           style: "abbreviated",
372           calendar: "gregory",
373         });
375         // Weekdays starting Sunday (7) to Saturday (6).
376         let weekdays = [7, 1, 2, 3, 4, 5, 6].map(day => displayNames.of(day));
377         dataToSend.weekdays = weekdays;
379         if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
380           dataToSend.isPrivate = true;
381           return dataToSend;
382         }
383         let sumEvents = await lazy.TrackingDBService.sumAllEvents();
384         let earliestDate =
385           await lazy.TrackingDBService.getEarliestRecordedDate();
386         let eventsByDate = await lazy.TrackingDBService.getEventsByDateRange(
387           aMessage.data.from,
388           aMessage.data.to
389         );
390         let largest = 0;
392         for (let result of eventsByDate) {
393           let count = result.getResultByName("count");
394           let type = result.getResultByName("type");
395           let timestamp = result.getResultByName("timestamp");
396           dataToSend[timestamp] = dataToSend[timestamp] || { total: 0 };
397           dataToSend[timestamp][idToTextMap.get(type)] = count;
398           dataToSend[timestamp].total += count;
399           // Record the largest amount of tracking events found per day,
400           // to create the tallest column on the graph and compare other days to.
401           if (largest < dataToSend[timestamp].total) {
402             largest = dataToSend[timestamp].total;
403           }
404         }
405         dataToSend.largest = largest;
406         dataToSend.earliestDate = earliestDate;
407         dataToSend.sumEvents = sumEvents;
409         return dataToSend;
411       case "FetchMonitorData":
412         return this.getMonitorData();
414       case "FetchUserLoginsData":
415         return this.getLoginData();
417       case "ClearMonitorCache":
418         monitorResponse = null;
419         break;
421       case "GetShowProxyCard":
422         let card = await this.shouldShowProxyCard();
423         return card;
425       case "RecordEntryPoint":
426         entrypoint = aMessage.data.entrypoint;
427         break;
429       case "FetchEntryPoint":
430         return entrypoint;
432       case "FetchVPNSubStatus":
433         return this.VPNSubStatus();
435       case "FetchShowVPNCard":
436         return lazy.BrowserUtils.shouldShowVPNPromo();
437     }
439     return undefined;
440   }