Bug 1933479 - Add tab close button on hover to vertical tabs when sidebar is collapse...
[gecko.git] / toolkit / components / antitracking / PurgeTrackerService.sys.mjs
blobf29b180e6c7c76091c750f92d420a322bb82e169
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 THREE_DAYS_MS = 3 * 24 * 60 * 1000;
9 const lazy = {};
11 XPCOMUtils.defineLazyServiceGetter(
12   lazy,
13   "gClassifier",
14   "@mozilla.org/url-classifier/dbservice;1",
15   "nsIURIClassifier"
17 XPCOMUtils.defineLazyServiceGetter(
18   lazy,
19   "gStorageActivityService",
20   "@mozilla.org/storage/activity-service;1",
21   "nsIStorageActivityService"
24 ChromeUtils.defineLazyGetter(lazy, "gClassifierFeature", () => {
25   return lazy.gClassifier.getFeatureByName("tracking-annotation");
26 });
28 ChromeUtils.defineLazyGetter(lazy, "logger", () => {
29   return console.createInstance({
30     prefix: "*** PurgeTrackerService:",
31     maxLogLevelPref: "privacy.purge_trackers.logging.level",
32   });
33 });
35 XPCOMUtils.defineLazyPreferenceGetter(
36   lazy,
37   "gConsiderEntityList",
38   "privacy.purge_trackers.consider_entity_list"
41 export function PurgeTrackerService() {}
43 PurgeTrackerService.prototype = {
44   classID: Components.ID("{90d1fd17-2018-4e16-b73c-a04a26fa6dd4}"),
45   QueryInterface: ChromeUtils.generateQI(["nsIPurgeTrackerService"]),
47   // Purging is batched for cookies to avoid clearing too much data
48   // at once. This flag tells us whether this is the first daily iteration.
49   _firstIteration: true,
51   // We can only know asynchronously if a host is matched by the tracking
52   // protection list, so we cache the result for faster future lookups.
53   _trackingState: new Map(),
55   observe(aSubject, aTopic) {
56     switch (aTopic) {
57       case "idle-daily":
58         // only allow one idle-daily listener to trigger until the list has been fully parsed.
59         Services.obs.removeObserver(this, "idle-daily");
60         this.purgeTrackingCookieJars();
61         break;
62       case "profile-after-change":
63         Services.obs.addObserver(this, "idle-daily");
64         break;
65     }
66   },
68   async isTracker(principal) {
69     if (principal.isNullPrincipal || principal.isSystemPrincipal) {
70       return false;
71     }
72     let host;
73     try {
74       host = principal.asciiHost;
75     } catch (error) {
76       return false;
77     }
79     if (!this._trackingState.has(host)) {
80       // Temporarily set to false to avoid doing several lookups if a site has
81       // several subframes on the same domain.
82       this._trackingState.set(host, false);
84       await new Promise(resolve => {
85         try {
86           lazy.gClassifier.asyncClassifyLocalWithFeatures(
87             principal.URI,
88             [lazy.gClassifierFeature],
89             Ci.nsIUrlClassifierFeature.blocklist,
90             list => {
91               if (list.length) {
92                 this._trackingState.set(host, true);
93               }
94               resolve();
95             }
96           );
97         } catch {
98           // Error in asyncClassifyLocalWithFeatures, it is not a tracker.
99           this._trackingState.set(host, false);
100           resolve();
101         }
102       });
103     }
105     return this._trackingState.get(host);
106   },
108   isAllowedThirdParty(firstPartyOriginNoSuffix, thirdPartyHost) {
109     let uri = Services.io.newURI(
110       `${firstPartyOriginNoSuffix}/?resource=${thirdPartyHost}`
111     );
112     lazy.logger.debug(`Checking entity list state for`, uri.spec);
113     return new Promise(resolve => {
114       try {
115         lazy.gClassifier.asyncClassifyLocalWithFeatures(
116           uri,
117           [lazy.gClassifierFeature],
118           Ci.nsIUrlClassifierFeature.entitylist,
119           list => {
120             let sameList = !!list.length;
121             lazy.logger.debug(`Is ${uri.spec} on the entity list?`, sameList);
122             resolve(sameList);
123           }
124         );
125       } catch {
126         resolve(false);
127       }
128     });
129   },
131   async maybePurgePrincipal(principal) {
132     let origin = principal.origin;
133     lazy.logger.debug(`Maybe purging ${origin}.`);
135     // First, check if any site with that base domain had received
136     // user interaction in the last N days.
137     let hasInteraction = this._baseDomainsWithInteraction.has(
138       principal.baseDomain
139     );
140     // Exit early unless we want to see if we're dealing with a tracker,
141     // for telemetry.
142     if (hasInteraction && !Services.telemetry.canRecordPrereleaseData) {
143       lazy.logger.debug(`${origin} has user interaction, exiting.`);
144       return;
145     }
147     // Second, confirm that we're looking at a tracker.
148     let isTracker = await this.isTracker(principal);
149     if (!isTracker) {
150       lazy.logger.debug(`${origin} is not a tracker, exiting.`);
151       return;
152     }
154     if (hasInteraction) {
155       let expireTimeMs = this._baseDomainsWithInteraction.get(
156         principal.baseDomain
157       );
159       // Collect how much longer the user interaction will be valid for, in hours.
160       let timeRemaining = Math.floor(
161         (expireTimeMs - Date.now()) / 1000 / 60 / 60 / 24
162       );
163       let permissionAgeHistogram = Services.telemetry.getHistogramById(
164         "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS"
165       );
166       permissionAgeHistogram.add(timeRemaining);
168       this._telemetryData.notPurged.add(principal.baseDomain);
170       lazy.logger.debug(`${origin} is a tracker with interaction, exiting.`);
171       return;
172     }
174     let isAllowedThirdParty = false;
175     if (
176       lazy.gConsiderEntityList ||
177       Services.telemetry.canRecordPrereleaseData
178     ) {
179       for (let firstPartyPrincipal of this._principalsWithInteraction) {
180         if (
181           await this.isAllowedThirdParty(
182             firstPartyPrincipal.originNoSuffix,
183             principal.asciiHost
184           )
185         ) {
186           isAllowedThirdParty = true;
187           break;
188         }
189       }
190     }
192     if (isAllowedThirdParty && lazy.gConsiderEntityList) {
193       lazy.logger.debug(
194         `${origin} has interaction on the entity list, exiting.`
195       );
196       return;
197     }
199     lazy.logger.log("Deleting data from:", origin);
201     await new Promise(resolve => {
202       Services.clearData.deleteDataFromPrincipal(
203         principal,
204         false,
205         Ci.nsIClearDataService.CLEAR_STATE_FOR_TRACKER_PURGING,
206         resolve
207       );
208     });
209     lazy.logger.log(`Data deleted from:`, origin);
211     this._telemetryData.purged.add(principal.baseDomain);
212   },
214   resetPurgeList() {
215     // We've reached the end of the cookies.
216     // Restore the idle-daily listener so it will purge again tomorrow.
217     Services.obs.addObserver(this, "idle-daily");
218     // Set the date to 0 so we will start at the beginning of the list next time.
219     Services.prefs.setStringPref(
220       "privacy.purge_trackers.date_in_cookie_database",
221       "0"
222     );
223   },
225   submitTelemetry() {
226     let { purged, notPurged, durationIntervals } = this._telemetryData;
227     let now = Date.now();
228     let lastPurge = Number(
229       Services.prefs.getStringPref("privacy.purge_trackers.last_purge", now)
230     );
232     let intervalHistogram = Services.telemetry.getHistogramById(
233       "COOKIE_PURGING_INTERVAL_HOURS"
234     );
235     let hoursBetween = Math.floor((now - lastPurge) / 1000 / 60 / 60);
236     intervalHistogram.add(hoursBetween);
238     Services.prefs.setStringPref(
239       "privacy.purge_trackers.last_purge",
240       now.toString()
241     );
243     let purgedHistogram = Services.telemetry.getHistogramById(
244       "COOKIE_PURGING_ORIGINS_PURGED"
245     );
246     purgedHistogram.add(purged.size);
248     let notPurgedHistogram = Services.telemetry.getHistogramById(
249       "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
250     );
251     notPurgedHistogram.add(notPurged.size);
253     let duration = durationIntervals
254       .map(([start, end]) => end - start)
255       .reduce((acc, cur) => acc + cur, 0);
257     let durationHistogram = Services.telemetry.getHistogramById(
258       "COOKIE_PURGING_DURATION_MS"
259     );
260     durationHistogram.add(duration);
261   },
263   /*
264    * Checks Cookie Permission a given 2 principals
265    * if either prinicpial cookie permissions are to prevent purging
266    * the function would return true
267    */
268   checkCookiePermissions(httpsPrincipal, httpPrincipal) {
269     let httpsCookiePermission;
270     let httpCookiePermission;
272     if (httpPrincipal) {
273       httpCookiePermission = Services.perms.testPermissionFromPrincipal(
274         httpPrincipal,
275         "cookie"
276       );
277     }
279     if (httpsPrincipal) {
280       httpsCookiePermission = Services.perms.testPermissionFromPrincipal(
281         httpsPrincipal,
282         "cookie"
283       );
284     }
286     if (
287       httpCookiePermission == Ci.nsICookiePermission.ACCESS_ALLOW ||
288       httpsCookiePermission == Ci.nsICookiePermission.ACCESS_ALLOW
289     ) {
290       return true;
291     }
293     return false;
294   },
295   /**
296    * This loops through all cookies saved in the database and checks if they are a tracking cookie, if it is it checks
297    * that they have an interaction permission which is still valid. If the Permission is not valid we delete all data
298    * associated with the site that owns that cookie.
299    */
300   async purgeTrackingCookieJars() {
301     let purgeEnabled = Services.prefs.getBoolPref(
302       "privacy.purge_trackers.enabled",
303       false
304     );
306     let sanitizeOnShutdownEnabled = Services.prefs.getBoolPref(
307       "privacy.sanitize.sanitizeOnShutdown",
308       false
309     );
311     let clearHistoryOnShutdown = Services.prefs.getBoolPref(
312       "privacy.clearOnShutdown.history",
313       false
314     );
316     let clearSiteSettingsOnShutdown = Services.prefs.getBoolPref(
317       "privacy.clearOnShutdown.siteSettings",
318       false
319     );
321     // This is a hotfix for bug 1672394. It avoids purging if the user has enabled mechanisms
322     // that regularly clear the storageAccessAPI permission, such as clearing history or
323     // "site settings" (permissions) on shutdown.
324     if (
325       sanitizeOnShutdownEnabled &&
326       (clearHistoryOnShutdown || clearSiteSettingsOnShutdown)
327     ) {
328       lazy.logger.log(
329         `
330         Purging canceled because interaction permissions are cleared on shutdown.
331         sanitizeOnShutdownEnabled: ${sanitizeOnShutdownEnabled},
332         clearHistoryOnShutdown: ${clearHistoryOnShutdown},
333         clearSiteSettingsOnShutdown: ${clearSiteSettingsOnShutdown},
334         `
335       );
336       this.resetPurgeList();
337       return;
338     }
340     // Purge cookie jars for following cookie behaviors.
341     //   * BEHAVIOR_REJECT_FOREIGN
342     //   * BEHAVIOR_LIMIT_FOREIGN
343     //   * BEHAVIOR_REJECT_TRACKER (ETP)
344     //   * BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN (dFPI)
345     let cookieBehavior = Services.cookies.getCookieBehavior(false);
347     let activeWithCookieBehavior =
348       cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN ||
349       cookieBehavior == Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN ||
350       cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER ||
351       cookieBehavior ==
352         Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
354     if (!activeWithCookieBehavior || !purgeEnabled) {
355       lazy.logger.log(
356         `returning early, activeWithCookieBehavior: ${activeWithCookieBehavior}, purgeEnabled: ${purgeEnabled}`
357       );
358       this.resetPurgeList();
359       return;
360     }
361     lazy.logger.log("Purging trackers enabled, beginning batch.");
362     // How many cookies to loop through in each batch before we quit
363     const MAX_PURGE_COUNT = Services.prefs.getIntPref(
364       "privacy.purge_trackers.max_purge_count",
365       100
366     );
368     if (this._firstIteration) {
369       this._telemetryData = {
370         durationIntervals: [],
371         purged: new Set(),
372         notPurged: new Set(),
373       };
375       this._baseDomainsWithInteraction = new Map();
376       this._principalsWithInteraction = [];
377       for (let perm of Services.perms.getAllWithTypePrefix(
378         "storageAccessAPI"
379       )) {
380         this._baseDomainsWithInteraction.set(
381           perm.principal.baseDomain,
382           perm.expireTime
383         );
384         this._principalsWithInteraction.push(perm.principal);
385       }
386     }
388     // Record how long this iteration took for telemetry.
389     // This is a tuple of start and end time, the second
390     // part will be added at the end of this function.
391     let duration = [Cu.now()];
393     /**
394      * We record the creationTime of the last cookie we looked at and
395      * start from there next time. This way even if new cookies are added or old ones are deleted we
396      * have a reliable way of finding our spot.
397      **/
398     let saved_date = Services.prefs.getStringPref(
399       "privacy.purge_trackers.date_in_cookie_database",
400       "0"
401     );
403     let maybeClearPrincipals = new Map();
405     // TODO We only need the host name and creationTime, this gives too much info. See bug 1610373.
406     let cookies = Services.cookies.getCookiesSince(saved_date);
407     cookies = cookies.slice(0, MAX_PURGE_COUNT);
409     for (let cookie of cookies) {
410       let httpPrincipal;
411       let httpsPrincipal;
413       let origin =
414         "http://" +
415         cookie.rawHost +
416         ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
417       try {
418         httpPrincipal =
419           Services.scriptSecurityManager.createContentPrincipalFromOrigin(
420             origin
421           );
422       } catch (e) {
423         lazy.logger.error(
424           `Creating principal from origin ${origin} led to error ${e}.`
425         );
426       }
428       origin =
429         "https://" +
430         cookie.rawHost +
431         ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
432       try {
433         httpsPrincipal =
434           Services.scriptSecurityManager.createContentPrincipalFromOrigin(
435             origin
436           );
437       } catch (e) {
438         lazy.logger.error(
439           `Creating principal from origin ${origin} led to error ${e}.`
440         );
441       }
443       // Checking to see if the Cookie Permissions is set to prevent Cookie from
444       // purging for either the HTTPS or HTTP conncetions
445       let purgeCheck = this.checkCookiePermissions(
446         httpsPrincipal,
447         httpPrincipal
448       );
450       if (httpPrincipal && !purgeCheck) {
451         maybeClearPrincipals.set(httpPrincipal.origin, httpPrincipal);
452       }
453       if (httpsPrincipal && !purgeCheck) {
454         maybeClearPrincipals.set(httpsPrincipal.origin, httpsPrincipal);
455       }
457       saved_date = cookie.creationTime;
458     }
460     // We only consider recently active storage and don't batch it,
461     // so only do this in the first iteration.
462     if (this._firstIteration) {
463       let startDate = Date.now() - THREE_DAYS_MS;
464       let storagePrincipals = lazy.gStorageActivityService.getActiveOrigins(
465         startDate * 1000,
466         Date.now() * 1000
467       );
469       for (let principal of storagePrincipals.enumerate()) {
470         // Check Principal Domains Cookie Permissions for both Schemes
471         // To ensure it does not bypass the cookie permissions set by the user
472         if (principal.schemeIs("https") || principal.schemeIs("http")) {
473           let otherURI;
474           let otherPrincipal;
476           if (principal.schemeIs("https")) {
477             otherURI = principal.URI.mutate().setScheme("http").finalize();
478           } else if (principal.schemeIs("http")) {
479             otherURI = principal.URI.mutate().setScheme("https").finalize();
480           }
482           try {
483             otherPrincipal =
484               Services.scriptSecurityManager.createContentPrincipal(
485                 otherURI,
486                 {}
487               );
488           } catch (e) {
489             lazy.logger.error(
490               `Creating principal from URI ${otherURI} led to error ${e}.`
491             );
492           }
494           if (!this.checkCookiePermissions(principal, otherPrincipal)) {
495             maybeClearPrincipals.set(principal.origin, principal);
496           }
497         } else {
498           maybeClearPrincipals.set(principal.origin, principal);
499         }
500       }
501     }
503     for (let principal of maybeClearPrincipals.values()) {
504       await this.maybePurgePrincipal(principal);
505     }
507     Services.prefs.setStringPref(
508       "privacy.purge_trackers.date_in_cookie_database",
509       saved_date
510     );
512     duration.push(Cu.now());
513     this._telemetryData.durationIntervals.push(duration);
515     // We've reached the end, no need to repeat again until next idle-daily.
516     if (!cookies.length || cookies.length < 100) {
517       lazy.logger.log(
518         "All cookie purging finished, resetting list until tomorrow."
519       );
520       this.resetPurgeList();
521       this.submitTelemetry();
522       this._firstIteration = true;
523       return;
524     }
526     lazy.logger.log("Batch finished, queueing next batch.");
527     this._firstIteration = false;
528     Services.tm.idleDispatchToMainThread(() => {
529       this.purgeTrackingCookieJars();
530     });
531   },