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;
11 XPCOMUtils.defineLazyServiceGetter(
14 "@mozilla.org/url-classifier/dbservice;1",
17 XPCOMUtils.defineLazyServiceGetter(
19 "gStorageActivityService",
20 "@mozilla.org/storage/activity-service;1",
21 "nsIStorageActivityService"
24 ChromeUtils.defineLazyGetter(lazy, "gClassifierFeature", () => {
25 return lazy.gClassifier.getFeatureByName("tracking-annotation");
28 ChromeUtils.defineLazyGetter(lazy, "logger", () => {
29 return console.createInstance({
30 prefix: "*** PurgeTrackerService:",
31 maxLogLevelPref: "privacy.purge_trackers.logging.level",
35 XPCOMUtils.defineLazyPreferenceGetter(
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) {
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();
62 case "profile-after-change":
63 Services.obs.addObserver(this, "idle-daily");
68 async isTracker(principal) {
69 if (principal.isNullPrincipal || principal.isSystemPrincipal) {
74 host = principal.asciiHost;
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 => {
86 lazy.gClassifier.asyncClassifyLocalWithFeatures(
88 [lazy.gClassifierFeature],
89 Ci.nsIUrlClassifierFeature.blocklist,
92 this._trackingState.set(host, true);
98 // Error in asyncClassifyLocalWithFeatures, it is not a tracker.
99 this._trackingState.set(host, false);
105 return this._trackingState.get(host);
108 isAllowedThirdParty(firstPartyOriginNoSuffix, thirdPartyHost) {
109 let uri = Services.io.newURI(
110 `${firstPartyOriginNoSuffix}/?resource=${thirdPartyHost}`
112 lazy.logger.debug(`Checking entity list state for`, uri.spec);
113 return new Promise(resolve => {
115 lazy.gClassifier.asyncClassifyLocalWithFeatures(
117 [lazy.gClassifierFeature],
118 Ci.nsIUrlClassifierFeature.entitylist,
120 let sameList = !!list.length;
121 lazy.logger.debug(`Is ${uri.spec} on the entity list?`, sameList);
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(
140 // Exit early unless we want to see if we're dealing with a tracker,
142 if (hasInteraction && !Services.telemetry.canRecordPrereleaseData) {
143 lazy.logger.debug(`${origin} has user interaction, exiting.`);
147 // Second, confirm that we're looking at a tracker.
148 let isTracker = await this.isTracker(principal);
150 lazy.logger.debug(`${origin} is not a tracker, exiting.`);
154 if (hasInteraction) {
155 let expireTimeMs = this._baseDomainsWithInteraction.get(
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
163 let permissionAgeHistogram = Services.telemetry.getHistogramById(
164 "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS"
166 permissionAgeHistogram.add(timeRemaining);
168 this._telemetryData.notPurged.add(principal.baseDomain);
170 lazy.logger.debug(`${origin} is a tracker with interaction, exiting.`);
174 let isAllowedThirdParty = false;
176 lazy.gConsiderEntityList ||
177 Services.telemetry.canRecordPrereleaseData
179 for (let firstPartyPrincipal of this._principalsWithInteraction) {
181 await this.isAllowedThirdParty(
182 firstPartyPrincipal.originNoSuffix,
186 isAllowedThirdParty = true;
192 if (isAllowedThirdParty && lazy.gConsiderEntityList) {
194 `${origin} has interaction on the entity list, exiting.`
199 lazy.logger.log("Deleting data from:", origin);
201 await new Promise(resolve => {
202 Services.clearData.deleteDataFromPrincipal(
205 Ci.nsIClearDataService.CLEAR_STATE_FOR_TRACKER_PURGING,
209 lazy.logger.log(`Data deleted from:`, origin);
211 this._telemetryData.purged.add(principal.baseDomain);
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",
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)
232 let intervalHistogram = Services.telemetry.getHistogramById(
233 "COOKIE_PURGING_INTERVAL_HOURS"
235 let hoursBetween = Math.floor((now - lastPurge) / 1000 / 60 / 60);
236 intervalHistogram.add(hoursBetween);
238 Services.prefs.setStringPref(
239 "privacy.purge_trackers.last_purge",
243 let purgedHistogram = Services.telemetry.getHistogramById(
244 "COOKIE_PURGING_ORIGINS_PURGED"
246 purgedHistogram.add(purged.size);
248 let notPurgedHistogram = Services.telemetry.getHistogramById(
249 "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
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"
260 durationHistogram.add(duration);
264 * Checks Cookie Permission a given 2 principals
265 * if either prinicpial cookie permissions are to prevent purging
266 * the function would return true
268 checkCookiePermissions(httpsPrincipal, httpPrincipal) {
269 let httpsCookiePermission;
270 let httpCookiePermission;
273 httpCookiePermission = Services.perms.testPermissionFromPrincipal(
279 if (httpsPrincipal) {
280 httpsCookiePermission = Services.perms.testPermissionFromPrincipal(
287 httpCookiePermission == Ci.nsICookiePermission.ACCESS_ALLOW ||
288 httpsCookiePermission == Ci.nsICookiePermission.ACCESS_ALLOW
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.
300 async purgeTrackingCookieJars() {
301 let purgeEnabled = Services.prefs.getBoolPref(
302 "privacy.purge_trackers.enabled",
306 let sanitizeOnShutdownEnabled = Services.prefs.getBoolPref(
307 "privacy.sanitize.sanitizeOnShutdown",
311 let clearHistoryOnShutdown = Services.prefs.getBoolPref(
312 "privacy.clearOnShutdown.history",
316 let clearSiteSettingsOnShutdown = Services.prefs.getBoolPref(
317 "privacy.clearOnShutdown.siteSettings",
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.
325 sanitizeOnShutdownEnabled &&
326 (clearHistoryOnShutdown || clearSiteSettingsOnShutdown)
330 Purging canceled because interaction permissions are cleared on shutdown.
331 sanitizeOnShutdownEnabled: ${sanitizeOnShutdownEnabled},
332 clearHistoryOnShutdown: ${clearHistoryOnShutdown},
333 clearSiteSettingsOnShutdown: ${clearSiteSettingsOnShutdown},
336 this.resetPurgeList();
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 ||
352 Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
354 if (!activeWithCookieBehavior || !purgeEnabled) {
356 `returning early, activeWithCookieBehavior: ${activeWithCookieBehavior}, purgeEnabled: ${purgeEnabled}`
358 this.resetPurgeList();
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",
368 if (this._firstIteration) {
369 this._telemetryData = {
370 durationIntervals: [],
372 notPurged: new Set(),
375 this._baseDomainsWithInteraction = new Map();
376 this._principalsWithInteraction = [];
377 for (let perm of Services.perms.getAllWithTypePrefix(
380 this._baseDomainsWithInteraction.set(
381 perm.principal.baseDomain,
384 this._principalsWithInteraction.push(perm.principal);
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()];
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.
398 let saved_date = Services.prefs.getStringPref(
399 "privacy.purge_trackers.date_in_cookie_database",
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) {
416 ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
419 Services.scriptSecurityManager.createContentPrincipalFromOrigin(
424 `Creating principal from origin ${origin} led to error ${e}.`
431 ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
434 Services.scriptSecurityManager.createContentPrincipalFromOrigin(
439 `Creating principal from origin ${origin} led to error ${e}.`
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(
450 if (httpPrincipal && !purgeCheck) {
451 maybeClearPrincipals.set(httpPrincipal.origin, httpPrincipal);
453 if (httpsPrincipal && !purgeCheck) {
454 maybeClearPrincipals.set(httpsPrincipal.origin, httpsPrincipal);
457 saved_date = cookie.creationTime;
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(
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")) {
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();
484 Services.scriptSecurityManager.createContentPrincipal(
490 `Creating principal from URI ${otherURI} led to error ${e}.`
494 if (!this.checkCookiePermissions(principal, otherPrincipal)) {
495 maybeClearPrincipals.set(principal.origin, principal);
498 maybeClearPrincipals.set(principal.origin, principal);
503 for (let principal of maybeClearPrincipals.values()) {
504 await this.maybePurgePrincipal(principal);
507 Services.prefs.setStringPref(
508 "privacy.purge_trackers.date_in_cookie_database",
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) {
518 "All cookie purging finished, resetting list until tomorrow."
520 this.resetPurgeList();
521 this.submitTelemetry();
522 this._firstIteration = true;
526 lazy.logger.log("Batch finished, queueing next batch.");
527 this._firstIteration = false;
528 Services.tm.idleDispatchToMainThread(() => {
529 this.purgeTrackingCookieJars();