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