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;
12 ChromeUtils.defineLazyGetter(lazy, "DB_PATH", function () {
13 return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
16 XPCOMUtils.defineLazyPreferenceGetter(
19 "privacy.socialtracking.block_cookies.enabled",
23 XPCOMUtils.defineLazyPreferenceGetter(
26 "privacy.fingerprintingProtection",
30 XPCOMUtils.defineLazyPreferenceGetter(
32 "milestoneMessagingEnabled",
33 "browser.contentblocking.cfr-milestone.enabled",
37 XPCOMUtils.defineLazyPreferenceGetter(
40 "browser.contentblocking.cfr-milestone.milestones",
46 XPCOMUtils.defineLazyPreferenceGetter(
49 "browser.contentblocking.cfr-milestone.milestone-achieved",
53 // How often we check if the user is eligible for seeing a "milestone"
54 // doorhanger. 24 hours by default.
55 XPCOMUtils.defineLazyPreferenceGetter(
57 "MILESTONE_UPDATE_INTERVAL",
58 "browser.contentblocking.cfr-milestone.update-interval",
62 ChromeUtils.defineESModuleGetters(lazy, {
63 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
64 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
68 * All SQL statements should be defined here.
72 "CREATE TABLE events (" +
73 "id INTEGER PRIMARY KEY, " +
74 "type INTEGER NOT NULL, " +
75 "count INTEGER NOT NULL, " +
80 "INSERT INTO events (type, count, timestamp) " +
81 "VALUES (:type, 1, date(:date));",
83 incrementEvent: "UPDATE events SET count = count + 1 WHERE id = :id;",
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);",
95 "SELECT * FROM events " +
96 "WHERE timestamp BETWEEN date(:dateFrom) AND date(:dateTo);",
98 sumAllEvents: "SELECT sum(count) FROM events;",
101 "SELECT timestamp FROM events ORDER BY timestamp ASC LIMIT 1;",
105 * Creates the database schema.
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.
128 waitingTasks: new Set(),
129 finishedShutdown: true,
132 await this._initPromise;
136 async _initialize() {
137 let db = await Sqlite.openConnection({ path: lazy.DB_PATH });
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) {
149 // await upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
152 await db.setSchemaVersion(SCHEMA_VERSION);
154 // Close the DB connection before passing the exception to the consumer.
159 lazy.AsyncShutdown.profileBeforeChange.addBlocker(
160 "TrackingDBService: Shutting down the content blocking database.",
161 () => this._shutdown()
163 this.finishedShutdown = false;
168 let db = await this.ensureDB();
169 this.finishedShutdown = true;
170 await Promise.all(Array.from(this.waitingTasks, task => task.finalize()));
174 async recordContentBlockingLog(data) {
175 if (this.finishedShutdown) {
176 // The database has already been closed.
179 let task = new lazy.DeferredTask(async () => {
181 await this.saveEvents(data);
183 this.waitingTasks.delete(task);
187 this.waitingTasks.add(task);
190 identifyType(events) {
192 let isTracker = false;
193 for (let [state, blocked] of events) {
196 Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT ||
197 state & Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT
204 Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT ||
206 Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT
208 result = Ci.nsITrackingDBService.FINGERPRINTERS_ID;
212 Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING
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.
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;
225 // If STP is enabled and either a social tracker or cookie is blocked.
226 lazy.social_enabled &&
228 Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER ||
230 Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT)
232 result = Ci.nsITrackingDBService.SOCIAL_ID;
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
238 result = Ci.nsITrackingDBService.TRACKERS_ID;
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
245 result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID;
248 Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION ||
249 state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL ||
250 state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN
252 result = Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID;
254 state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT
256 result = Ci.nsITrackingDBService.CRYPTOMINERS_ID;
258 state & Ci.nsIWebProgressListener.STATE_PURGED_BOUNCETRACKER
260 result = Ci.nsITrackingDBService.BOUNCETRACKERS_ID;
264 // if a cookie is blocked for any reason, and it is identified as a tracker,
265 // then add to the tracking cookies count.
267 result == Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID &&
270 result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID;
277 * Saves data rows to the DB.
279 * An array of JS objects representing row items to save.
281 async saveEvents(data) {
282 let db = await this.ensureDB();
283 let log = JSON.parse(data);
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]);
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, {
301 let todayEntry = row[0];
303 // If previous events happened today (local time), aggregate them.
305 let id = todayEntry.getResultByName("id");
306 await db.executeCached(SQL.incrementEvent, { id });
308 // Event is created on a new day, add a new entry.
309 await db.executeCached(SQL.addEvent, { type, date: today });
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.
321 !lazy.milestoneMessagingEnabled ||
323 Date.now() - this.lastChecked < lazy.MILESTONE_UPDATE_INTERVAL)
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];
339 // Show the milestone message if the user is not too close to the next milestone.
340 // Or if there is no next milestone.
343 (!nextMilestone || nextMilestone - totalSaved > 3000) &&
344 (!lazy.oldMilestone || lazy.oldMilestone < reachedMilestone)
346 Services.obs.notifyObservers(
349 event: "ContentBlockingMilestone",
352 "SiteProtection:ContentBlockingMilestone"
358 let db = await this.ensureDB();
359 await removeAllRecords(db);
362 async clearSince(date) {
363 let db = await this.ensureDB();
364 date = new Date(date).toISOString();
365 await removeRecordsSince(db, date);
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 });
375 async sumAllEvents() {
376 let db = await this.ensureDB();
377 let results = await db.execute(SQL.sumAllEvents);
381 let total = results[0].getResultByName("sum(count)");
385 async getEarliestRecordedDate() {
386 let db = await this.ensureDB();
387 let date = await db.execute(SQL.getEarliestDate);
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;