Bug 1942239 - Add option to explicitly enable incremental origin initialization in...
[gecko.git] / toolkit / components / antitracking / TrackingDBService.sys.mjs
blobb42604b2111603d9c54f96fe936f4064c52abb8a
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";
6 import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs";
8 const SCHEMA_VERSION = 1;
10 const lazy = {};
12 ChromeUtils.defineLazyGetter(lazy, "DB_PATH", function () {
13   return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
14 });
16 XPCOMUtils.defineLazyPreferenceGetter(
17   lazy,
18   "social_enabled",
19   "privacy.socialtracking.block_cookies.enabled",
20   false
23 XPCOMUtils.defineLazyPreferenceGetter(
24   lazy,
25   "fpp_enabled",
26   "privacy.fingerprintingProtection",
27   false
30 XPCOMUtils.defineLazyPreferenceGetter(
31   lazy,
32   "milestoneMessagingEnabled",
33   "browser.contentblocking.cfr-milestone.enabled",
34   false
37 XPCOMUtils.defineLazyPreferenceGetter(
38   lazy,
39   "milestones",
40   "browser.contentblocking.cfr-milestone.milestones",
41   "[]",
42   null,
43   JSON.parse
46 XPCOMUtils.defineLazyPreferenceGetter(
47   lazy,
48   "oldMilestone",
49   "browser.contentblocking.cfr-milestone.milestone-achieved",
50   0
53 // How often we check if the user is eligible for seeing a "milestone"
54 // doorhanger. 24 hours by default.
55 XPCOMUtils.defineLazyPreferenceGetter(
56   lazy,
57   "MILESTONE_UPDATE_INTERVAL",
58   "browser.contentblocking.cfr-milestone.update-interval",
59   24 * 60 * 60 * 1000
62 ChromeUtils.defineESModuleGetters(lazy, {
63   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
64   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
65 });
67 /**
68  * All SQL statements should be defined here.
69  */
70 const SQL = {
71   createEvents:
72     "CREATE TABLE events (" +
73     "id INTEGER PRIMARY KEY, " +
74     "type INTEGER NOT NULL, " +
75     "count INTEGER NOT NULL, " +
76     "timestamp DATE " +
77     ");",
79   addEvent:
80     "INSERT INTO events (type, count, timestamp) " +
81     "VALUES (:type, 1, date(:date));",
83   incrementEvent: "UPDATE events SET count = count + 1 WHERE id = :id;",
85   selectByTypeAndDate:
86     "SELECT * FROM events " +
87     "WHERE type = :type " +
88     "AND timestamp = date(:date);",
90   deleteEventsRecords: "DELETE FROM events;",
92   removeRecordsSince: "DELETE FROM events WHERE timestamp >= date(:date);",
94   selectByDateRange:
95     "SELECT * FROM events " +
96     "WHERE timestamp BETWEEN date(:dateFrom) AND date(:dateTo);",
98   sumAllEvents: "SELECT sum(count) FROM events;",
100   getEarliestDate:
101     "SELECT timestamp FROM events ORDER BY timestamp ASC LIMIT 1;",
105  * Creates the database schema.
106  */
107 async function createDatabase(db) {
108   await db.execute(SQL.createEvents);
111 async function removeAllRecords(db) {
112   await db.execute(SQL.deleteEventsRecords);
115 async function removeRecordsSince(db, date) {
116   await db.execute(SQL.removeRecordsSince, { date });
119 export function TrackingDBService() {
120   this._initPromise = this._initialize();
123 TrackingDBService.prototype = {
124   classID: Components.ID("{3c9c43b6-09eb-4ed2-9b87-e29f4221eef0}"),
125   QueryInterface: ChromeUtils.generateQI(["nsITrackingDBService"]),
126   // This is the connection to the database, opened in _initialize and closed on _shutdown.
127   _db: null,
128   waitingTasks: new Set(),
129   finishedShutdown: true,
131   async ensureDB() {
132     await this._initPromise;
133     return this._db;
134   },
136   async _initialize() {
137     let db = await Sqlite.openConnection({ path: lazy.DB_PATH });
139     try {
140       // Check to see if we need to perform any migrations.
141       let dbVersion = parseInt(await db.getSchemaVersion());
143       // getSchemaVersion() returns a 0 int if the schema
144       // version is undefined.
145       if (dbVersion === 0) {
146         await createDatabase(db);
147       } else if (dbVersion < SCHEMA_VERSION) {
148         // TODO
149         // await upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
150       }
152       await db.setSchemaVersion(SCHEMA_VERSION);
153     } catch (e) {
154       // Close the DB connection before passing the exception to the consumer.
155       await db.close();
156       throw e;
157     }
159     lazy.AsyncShutdown.profileBeforeChange.addBlocker(
160       "TrackingDBService: Shutting down the content blocking database.",
161       () => this._shutdown()
162     );
163     this.finishedShutdown = false;
164     this._db = db;
165   },
167   async _shutdown() {
168     let db = await this.ensureDB();
169     this.finishedShutdown = true;
170     await Promise.all(Array.from(this.waitingTasks, task => task.finalize()));
171     await db.close();
172   },
174   async recordContentBlockingLog(data) {
175     if (this.finishedShutdown) {
176       // The database has already been closed.
177       return;
178     }
179     let task = new lazy.DeferredTask(async () => {
180       try {
181         await this.saveEvents(data);
182       } finally {
183         this.waitingTasks.delete(task);
184       }
185     }, 0);
186     task.arm();
187     this.waitingTasks.add(task);
188   },
190   identifyType(events) {
191     let result = null;
192     let isTracker = false;
193     for (let [state, blocked] of events) {
194       if (
195         state &
196           Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT ||
197         state & Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT
198       ) {
199         isTracker = true;
200       }
201       if (blocked) {
202         if (
203           state &
204             Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT ||
205           state &
206             Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT
207         ) {
208           result = Ci.nsITrackingDBService.FINGERPRINTERS_ID;
209         } else if (
210           lazy.fpp_enabled &&
211           state &
212             Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING
213         ) {
214           // The suspicious fingerprinting event gets filed in standard windows
215           // regardless of whether the fingerprinting protection is enabled. To
216           // avoid recording the case where our protection doesn't apply, we
217           // only record blocking suspicious fingerprinting if the
218           // fingerprinting protection is enabled in the normal windows.
219           //
220           // TODO(Bug 1864909): We don't need to check if fingerprinting
221           // protection is enabled once the event only gets filed when
222           // fingerprinting protection is enabled for the context.
223           result = Ci.nsITrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID;
224         } else if (
225           // If STP is enabled and either a social tracker or cookie is blocked.
226           lazy.social_enabled &&
227           (state &
228             Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER ||
229             state &
230               Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT)
231         ) {
232           result = Ci.nsITrackingDBService.SOCIAL_ID;
233         } else if (
234           // If there is a tracker blocked. If there is a social tracker blocked, but STP is not enabled.
235           state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT ||
236           state & Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT
237         ) {
238           result = Ci.nsITrackingDBService.TRACKERS_ID;
239         } else if (
240           // If a tracking cookie was blocked attribute it to tracking cookies.
241           // This includes social tracking cookies since STP is not enabled.
242           state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER ||
243           state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER
244         ) {
245           result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID;
246         } else if (
247           state &
248             Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION ||
249           state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL ||
250           state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN
251         ) {
252           result = Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID;
253         } else if (
254           state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT
255         ) {
256           result = Ci.nsITrackingDBService.CRYPTOMINERS_ID;
257         } else if (
258           state & Ci.nsIWebProgressListener.STATE_PURGED_BOUNCETRACKER
259         ) {
260           result = Ci.nsITrackingDBService.BOUNCETRACKERS_ID;
261         }
262       }
263     }
264     // if a cookie is blocked for any reason, and it is identified as a tracker,
265     // then add to the tracking cookies count.
266     if (
267       result == Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID &&
268       isTracker
269     ) {
270       result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID;
271     }
273     return result;
274   },
276   /**
277    * Saves data rows to the DB.
278    * @param data
279    *        An array of JS objects representing row items to save.
280    */
281   async saveEvents(data) {
282     let db = await this.ensureDB();
283     let log = JSON.parse(data);
284     try {
285       await db.executeTransaction(async () => {
286         for (let thirdParty in log) {
287           // "type" will be undefined if there is no blocking event, or 0 if it is a
288           // cookie which is not a tracking cookie. These should not be added to the database.
289           let type = this.identifyType(log[thirdParty]);
290           if (type) {
291             // Send the blocked event to Telemetry
292             Glean.contentblocking.trackersBlockedCount.add(1);
294             // today is a date "YYY-MM-DD" which can compare with what is
295             // already saved in the database.
296             let today = new Date().toISOString().split("T")[0];
297             let row = await db.executeCached(SQL.selectByTypeAndDate, {
298               type,
299               date: today,
300             });
301             let todayEntry = row[0];
303             // If previous events happened today (local time), aggregate them.
304             if (todayEntry) {
305               let id = todayEntry.getResultByName("id");
306               await db.executeCached(SQL.incrementEvent, { id });
307             } else {
308               // Event is created on a new day, add a new entry.
309               await db.executeCached(SQL.addEvent, { type, date: today });
310             }
311           }
312         }
313       });
314     } catch (e) {
315       console.error(e);
316     }
318     // If milestone CFR messaging is not enabled we don't need to update the milestone pref or send the event.
319     // We don't do this check too frequently, for performance reasons.
320     if (
321       !lazy.milestoneMessagingEnabled ||
322       (this.lastChecked &&
323         Date.now() - this.lastChecked < lazy.MILESTONE_UPDATE_INTERVAL)
324     ) {
325       return;
326     }
327     this.lastChecked = Date.now();
328     let totalSaved = await this.sumAllEvents();
330     let reachedMilestone = null;
331     let nextMilestone = null;
332     for (let [index, milestone] of lazy.milestones.entries()) {
333       if (totalSaved >= milestone) {
334         reachedMilestone = milestone;
335         nextMilestone = lazy.milestones[index + 1];
336       }
337     }
339     // Show the milestone message if the user is not too close to the next milestone.
340     // Or if there is no next milestone.
341     if (
342       reachedMilestone &&
343       (!nextMilestone || nextMilestone - totalSaved > 3000) &&
344       (!lazy.oldMilestone || lazy.oldMilestone < reachedMilestone)
345     ) {
346       Services.obs.notifyObservers(
347         {
348           wrappedJSObject: {
349             event: "ContentBlockingMilestone",
350           },
351         },
352         "SiteProtection:ContentBlockingMilestone"
353       );
354     }
355   },
357   async clearAll() {
358     let db = await this.ensureDB();
359     await removeAllRecords(db);
360   },
362   async clearSince(date) {
363     let db = await this.ensureDB();
364     date = new Date(date).toISOString();
365     await removeRecordsSince(db, date);
366   },
368   async getEventsByDateRange(dateFrom, dateTo) {
369     let db = await this.ensureDB();
370     dateFrom = new Date(dateFrom).toISOString();
371     dateTo = new Date(dateTo).toISOString();
372     return db.execute(SQL.selectByDateRange, { dateFrom, dateTo });
373   },
375   async sumAllEvents() {
376     let db = await this.ensureDB();
377     let results = await db.execute(SQL.sumAllEvents);
378     if (!results[0]) {
379       return 0;
380     }
381     let total = results[0].getResultByName("sum(count)");
382     return total || 0;
383   },
385   async getEarliestRecordedDate() {
386     let db = await this.ensureDB();
387     let date = await db.execute(SQL.getEarliestDate);
388     if (!date[0]) {
389       return null;
390     }
391     let earliestDate = date[0].getResultByName("timestamp");
393     // All of our dates are recorded as 00:00 GMT, add 12 hours to the timestamp
394     // to ensure we display the correct date no matter the user's location.
395     let hoursInMS12 = 12 * 60 * 60 * 1000;
396     let earliestDateInMS = new Date(earliestDate).getTime() + hoursInMS12;
398     return earliestDateInMS || null;
399   },