summaryrefslogtreecommitdiffstats
path: root/src/js/storage.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/js/storage.js707
1 files changed, 707 insertions, 0 deletions
diff --git a/src/js/storage.js b/src/js/storage.js
new file mode 100644
index 0000000..9074a41
--- /dev/null
+++ b/src/js/storage.js
@@ -0,0 +1,707 @@
+/*
+ * This file is part of Privacy Badger <https://www.eff.org/privacybadger>
+ * Copyright (C) 2014 Electronic Frontier Foundation
+ *
+ * Privacy Badger is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Privacy Badger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Badger. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* globals badger:false, log:false */
+
+var constants = require("constants");
+var utils = require("utils");
+
+require.scopes.storage = (function() {
+
+
+/**
+ * # Storage Objects
+ *
+ * snitch_map is our collection of potential tracking base_domains.
+ * The key is a base domain (ETLD+1) and the value is an array of first
+ * party domains on which this tracker has been seen.
+ * it looks like this:
+ * {
+ * "third-party.com": ["a.com", "b.com", "c.com"],
+ * "eviltracker.net": ["eff.org", "a.com"]
+ * }
+ *
+ * action_map is where we store the action for each domain that we have
+ * decided on an action for. Each subdomain gets its own entry. For example:
+ * {
+ * "google.com": { heuristicAction: "block", dnt: false, userAction: ""}
+ * "fonts.google.com": { heuristicAction: "cookieblock", dnt: false, userAction: ""}
+ * "apis.google.com": { heuristicAction: "cookieblock", dnt: false, userAction: "user_block"}
+ * "widget.eff.org": { heuristicAction: "block", dnt: true, userAction: ""}
+ * }
+ *
+ * cookieblock_list is where we store the current yellowlist as
+ * downloaded from eff.org. The keys are the domains which should be "cookieblocked".
+ * The values are simply 'true'. For example:
+ * {
+ * "maps.google.com": true,
+ * "creativecommons.org": true,
+ * }
+ *
+ */
+
+function BadgerPen(callback) {
+ let self = this;
+
+ if (!callback) {
+ callback = function () {};
+ }
+
+ // initialize from extension local storage
+ chrome.storage.local.get(self.KEYS, function (store) {
+ self.KEYS.forEach(key => {
+ if (store.hasOwnProperty(key)) {
+ self[key] = new BadgerStorage(key, store[key]);
+ } else {
+ let storageObj = new BadgerStorage(key, {});
+ self[key] = storageObj;
+ _syncStorage(storageObj);
+ }
+ });
+
+ if (!chrome.storage.managed) {
+ callback(self);
+ return;
+ }
+
+ // see if we have any enterprise/admin/group policy overrides
+ chrome.storage.managed.get(null, function (managedStore) {
+ if (chrome.runtime.lastError) {
+ // ignore "Managed storage manifest not found" errors in Firefox
+ }
+
+ if (_.isObject(managedStore)) {
+ let settings = {};
+ for (let key in badger.defaultSettings) {
+ if (managedStore.hasOwnProperty(key)) {
+ settings[key] = managedStore[key];
+ }
+ }
+ self.settings_map.merge(settings);
+ }
+
+ callback(self);
+ });
+ });
+}
+
+BadgerPen.prototype = {
+ KEYS: [
+ "snitch_map",
+ "action_map",
+ "cookieblock_list",
+ "dnt_hashes",
+ "settings_map",
+ "private_storage", // misc. utility settings, not for export
+ ],
+
+ getStore: function (key) {
+ if (this.hasOwnProperty(key)) {
+ return this[key];
+ }
+ console.error("Can't initialize cache from getStore. You are using this API improperly");
+ },
+
+ /**
+ * Reset the snitch map and action map, forgetting all data the badger has
+ * learned from browsing.
+ */
+ clearTrackerData: function () {
+ let self = this;
+ ['snitch_map', 'action_map'].forEach(key => {
+ self.getStore(key).updateObject({});
+ });
+ },
+
+ /**
+ * Get the current presumed action for a specific fully qualified domain name (FQDN),
+ * ignoring any rules for subdomains below or above it
+ *
+ * @param {(Object|String)} domain domain object from action_map
+ * @param {Boolean} [ignoreDNT] whether to ignore DNT status
+ * @returns {String} the presumed action for this FQDN
+ */
+ getAction: function (domain, ignoreDNT) {
+ if (!badger.isCheckingDNTPolicyEnabled()) {
+ ignoreDNT = true;
+ }
+
+ if (_.isString(domain)) {
+ domain = this.getStore('action_map').getItem(domain) || {};
+ }
+ if (domain.userAction) { return domain.userAction; }
+ if (domain.dnt && !ignoreDNT) { return constants.DNT; }
+ if (domain.heuristicAction) { return domain.heuristicAction; }
+ return constants.NO_TRACKING;
+ },
+
+ touchDNTRecheckTime: function(domain, time) {
+ this._setupDomainAction(domain, time, "nextUpdateTime");
+ },
+
+ getNextUpdateForDomain: function(domain) {
+ var action_map = this.getStore('action_map');
+ if (action_map.hasItem(domain)) {
+ return action_map.getItem(domain).nextUpdateTime;
+ } else {
+ return 0;
+ }
+ },
+
+ /**
+ * Updates the yellowlist to the provided array of domains.
+ *
+ * For each added domain, sets it to be cookieblocked
+ * if its parent domain is set to be blocked.
+ *
+ * @param {Array} newDomains domains to use for the new yellowlist
+ */
+ updateYellowlist: function (newDomains) {
+ let self = this,
+ actionMap = self.getStore('action_map'),
+ ylistStorage = self.getStore('cookieblock_list'),
+ oldDomains = ylistStorage.keys();
+
+ let addedDomains = _.difference(newDomains, oldDomains),
+ removedDomains = _.difference(oldDomains, newDomains);
+
+ log('removing from cookie blocklist:', removedDomains);
+ removedDomains.forEach(function (domain) {
+ ylistStorage.deleteItem(domain);
+
+ const base = window.getBaseDomain(domain);
+ // "subdomains" include the domain itself
+ for (const subdomain of actionMap.keys()) {
+ if (window.getBaseDomain(subdomain) == base) {
+ if (self.getAction(subdomain) != constants.NO_TRACKING) {
+ badger.heuristicBlocking.blocklistOrigin(base, subdomain);
+ }
+ }
+ }
+ });
+
+ log('adding to cookie blocklist:', addedDomains);
+ addedDomains.forEach(function (domain) {
+ ylistStorage.setItem(domain, true);
+
+ const base = window.getBaseDomain(domain);
+ if (actionMap.hasItem(base)) {
+ const action = actionMap.getItem(base).heuristicAction;
+ // if the domain's base domain is marked for blocking
+ if (action == constants.BLOCK || action == constants.COOKIEBLOCK) {
+ // cookieblock the domain
+ self.setupHeuristicAction(domain, constants.COOKIEBLOCK);
+ }
+ }
+ });
+ },
+
+ /**
+ * Update DNT policy hashes
+ */
+ updateDntHashes: function (hashes) {
+ var dnt_hashes = this.getStore('dnt_hashes');
+ dnt_hashes.updateObject(_.invert(hashes));
+ },
+
+ /**
+ * Looks up whether an FQDN would get cookieblocked,
+ * ignoring user overrides and the FQDN's current status.
+ *
+ * @param {String} fqdn the FQDN we want to look up
+ *
+ * @return {Boolean}
+ */
+ wouldGetCookieblocked: function (fqdn) {
+ // cookieblock if a "parent" domain of the fqdn is on the yellowlist
+ let set = false,
+ ylistStorage = this.getStore('cookieblock_list'),
+ // ignore base domains when exploding to work around PSL TLDs:
+ // still want to cookieblock somedomain.googleapis.com with only
+ // googleapis.com (and not somedomain.googleapis.com itself) on the ylist
+ subdomains = utils.explodeSubdomains(fqdn, true);
+
+ for (let i = 0; i < subdomains.length; i++) {
+ if (ylistStorage.hasItem(subdomains[i])) {
+ set = true;
+ break;
+ }
+ }
+
+ return set;
+ },
+
+ /**
+ * Find the best action to take for an FQDN, assuming it is third party and
+ * Privacy Badger is enabled. Traverse the action list for the FQDN and each
+ * of its subdomains and then takes the most appropriate action
+ *
+ * @param {String} fqdn the FQDN we want to determine the action for
+ * @returns {String} the best action for the FQDN
+ */
+ getBestAction: function (fqdn) {
+ let best_action = constants.NO_TRACKING;
+ let subdomains = utils.explodeSubdomains(fqdn);
+ let action_map = this.getStore('action_map');
+
+ function getScore(action) {
+ switch (action) {
+ case constants.NO_TRACKING:
+ return 0;
+ case constants.ALLOW:
+ return 1;
+ case constants.BLOCK:
+ return 2;
+ case constants.COOKIEBLOCK:
+ return 3;
+ case constants.DNT:
+ return 4;
+ case constants.USER_ALLOW:
+ case constants.USER_BLOCK:
+ case constants.USER_COOKIEBLOCK:
+ return 5;
+ }
+ }
+
+ // Loop through each subdomain we have a rule for
+ // from least (base domain) to most (FQDN) specific
+ // and keep the one which has the best score.
+ for (let i = subdomains.length; i >= 0; i--) {
+ let domain = subdomains[i];
+ if (action_map.hasItem(domain)) {
+ let action = this.getAction(
+ action_map.getItem(domain),
+ // ignore DNT unless it's directly on the FQDN being checked
+ domain != fqdn
+ );
+ if (getScore(action) >= getScore(best_action)) {
+ best_action = action;
+ }
+ }
+ }
+
+ return best_action;
+ },
+
+ /**
+ * Find every domain in the action_map where the presumed action would be {selector}
+ *
+ * @param {String} selector the action to select by
+ * @return {Array} an array of FQDN strings
+ */
+ getAllDomainsByPresumedAction: function (selector) {
+ var action_map = this.getStore('action_map');
+ var relevantDomains = [];
+ for (var domain in action_map.getItemClones()) {
+ if (selector == this.getAction(domain)) {
+ relevantDomains.push(domain);
+ }
+ }
+ return relevantDomains;
+ },
+
+ /**
+ * Get all tracking domains from action_map.
+ *
+ * @return {Object} An object with domains as keys and actions as values.
+ */
+ getTrackingDomains: function () {
+ let action_map = this.getStore('action_map');
+ let origins = {};
+
+ for (let domain in action_map.getItemClones()) {
+ let action = badger.storage.getBestAction(domain);
+ if (action != constants.NO_TRACKING) {
+ origins[domain] = action;
+ }
+ }
+
+ return origins;
+ },
+
+ /**
+ * Set up an action for a domain of the given action type in action_map
+ *
+ * @param {String} domain the domain to set the action for
+ * @param {String} action the action to take e.g. BLOCK || COOKIEBLOCK || DNT
+ * @param {String} actionType the type of action we are setting, one of "userAction", "heuristicAction", "dnt"
+ * @private
+ */
+ _setupDomainAction: function (domain, action, actionType) {
+ let msg = "action_map['%s'].%s = %s",
+ action_map = this.getStore("action_map"),
+ actionObj = {};
+
+ if (action_map.hasItem(domain)) {
+ actionObj = action_map.getItem(domain);
+ msg = "Updating " + msg;
+ } else {
+ actionObj = _newActionMapObject();
+ msg = "Initializing " + msg;
+ }
+ actionObj[actionType] = action;
+
+ if (window.DEBUG) { // to avoid needless JSON.stringify calls
+ log(msg, domain, actionType, JSON.stringify(action));
+ }
+ action_map.setItem(domain, actionObj);
+ },
+
+ /**
+ * Add a heuristic action for a domain
+ *
+ * @param {String} domain Domain to add
+ * @param {String} action The heuristic action to take
+ */
+ setupHeuristicAction: function(domain, action) {
+ this._setupDomainAction(domain, action, "heuristicAction");
+ },
+
+ /**
+ * Set up a domain for DNT
+ *
+ * @param {String} domain Domain to add
+ */
+ setupDNT: function(domain) {
+ this._setupDomainAction(domain, true, "dnt");
+ },
+
+ /**
+ * Remove DNT setting from a domain*
+ * @param {String} domain FQDN string
+ */
+ revertDNT: function(domain) {
+ this._setupDomainAction(domain, false, "dnt");
+ },
+
+ /**
+ * Add a heuristic action for a domain
+ *
+ * @param {String} domain Domain to add
+ * @param {String} action The heuristic action to take
+ */
+ setupUserAction: function(domain, action) {
+ this._setupDomainAction(domain, action, "userAction");
+ },
+
+ /**
+ * Remove user set action from a domain
+ * @param {String} domain FQDN string
+ */
+ revertUserAction: function(domain) {
+ this._setupDomainAction(domain, "", "userAction");
+
+ // if Privacy Badger never recorded tracking for this domain,
+ // remove the domain's entry from Privacy Badger's database
+ const actionMap = this.getStore("action_map");
+ if (actionMap.getItem(domain).heuristicAction == "") {
+ log("Removing %s from action_map", domain);
+ actionMap.deleteItem(domain);
+ }
+ },
+
+ /**
+ * Removes a base domain and its subdomains from snitch and action maps.
+ * Preserves action map entries with user overrides.
+ *
+ * @param {String} base_domain
+ */
+ forget: function (base_domain) {
+ let self = this,
+ dot_base = '.' + base_domain,
+ actionMap = self.getStore('action_map'),
+ actions = actionMap.getItemClones(),
+ snitchMap = self.getStore('snitch_map');
+
+ if (snitchMap.getItem(base_domain)) {
+ log("Removing %s from snitch_map", base_domain);
+ badger.storage.getStore("snitch_map").deleteItem(base_domain);
+ }
+
+ for (let domain in actions) {
+ if (domain == base_domain || domain.endsWith(dot_base)) {
+ if (actions[domain].userAction == "") {
+ log("Removing %s from action_map", domain);
+ actionMap.deleteItem(domain);
+ }
+ }
+ }
+ }
+};
+
+/**
+ * @returns {{userAction: null, dnt: null, heuristicAction: null}}
+ * @private
+ */
+var _newActionMapObject = function() {
+ return {
+ userAction: "",
+ dnt: false,
+ heuristicAction: "",
+ nextUpdateTime: 0
+ };
+};
+
+/**
+ * Privacy Badger Storage Object. Has methods for getting, setting and deleting
+ * should be used for all storage needs, transparently handles data presistence
+ * syncing and private browsing.
+ * Usage:
+ * example_map = getStore('example_map');
+ * # instance of BadgerStorage
+ * example_map.setItem('foo', 'bar')
+ * # null
+ * example_map
+ * # { foo: "bar" }
+ * example_map.hasItem('foo')
+ * # true
+ * example_map.getItem('foo');
+ * # 'bar'
+ * example_map.getItem('not_real');
+ * # undefined
+ * example_map.deleteItem('foo');
+ * # null
+ * example_map.hasItem('foo');
+ * # false
+ *
+ */
+
+/**
+ * BadgerStorage constructor
+ * *DO NOT USE DIRECTLY* Instead call `getStore(name)`
+ * @param {String} name - the name of the storage object
+ * @param {Object} seed - the base object which we are instantiating from
+ */
+var BadgerStorage = function(name, seed) {
+ this.name = name;
+ this._store = seed;
+};
+
+BadgerStorage.prototype = {
+ /**
+ * Check if this storage object has an item
+ *
+ * @param {String} key - the key for the item
+ * @return {Boolean}
+ */
+ hasItem: function(key) {
+ var self = this;
+ return self._store.hasOwnProperty(key);
+ },
+
+ /**
+ * Get an item
+ *
+ * @param {String} key - the key for the item
+ * @return {?*} the value for that key or null
+ */
+ getItem: function(key) {
+ var self = this;
+ if (self.hasItem(key)) {
+ return self._store[key];
+ } else {
+ return null;
+ }
+ },
+
+ /**
+ * Get all items in the object as a copy
+ *
+ * @return {*} the items in badgerObject
+ */
+ getItemClones: function() {
+ var self = this;
+ return JSON.parse(JSON.stringify(self._store));
+ },
+
+ /**
+ * Set an item
+ *
+ * @param {String} key - the key for the item
+ * @param {*} value - the new value
+ */
+ setItem: function(key,value) {
+ var self = this;
+ self._store[key] = value;
+ // Async call to syncStorage.
+ setTimeout(function() {
+ _syncStorage(self);
+ }, 0);
+ },
+
+ /**
+ * Delete an item
+ *
+ * @param {String} key - the key for the item
+ */
+ deleteItem: function(key) {
+ var self = this;
+ delete self._store[key];
+ // Async call to syncStorage.
+ setTimeout(function() {
+ _syncStorage(self);
+ }, 0);
+ },
+
+ /**
+ * Update the entire object that this instance is storing
+ */
+ updateObject: function(object) {
+ var self = this;
+ self._store = object;
+ // Async call to syncStorage.
+ setTimeout(function() {
+ _syncStorage(self);
+ }, 0);
+ },
+
+ /**
+ * @returns {Array} this storage object's store keys
+ */
+ keys: function () {
+ return Object.keys(this._store);
+ },
+
+ /**
+ * When a user imports a tracker and settings list via the Import function,
+ * we want to overwrite any existing settings, while simultaneously merging
+ * in any new information (i.e. the list of disabled site domains). In order
+ * to do this, we need different logic for each of the storage maps based on
+ * their internal structure. The three cases in this function handle each of
+ * the three storage maps that can be exported.
+ *
+ * @param {Object} mapData The object containing storage map data to merge
+ */
+ merge: function (mapData) {
+ const self = this;
+
+ if (self.name == "settings_map") {
+ for (let prop in mapData) {
+ // combine array settings via intersection/union
+ if (prop == "disabledSites" || prop == "widgetReplacementExceptions") {
+ self._store[prop] = _.union(self._store[prop], mapData[prop]);
+
+ // string/array map
+ } else if (prop == "widgetSiteAllowlist") {
+ // for every site host in the import
+ for (let site in mapData[prop]) {
+ // combine exception arrays
+ self._store[prop][site] = _.union(
+ self._store[prop][site],
+ mapData[prop][site]
+ );
+ }
+
+ // default: overwrite existing setting with setting from import
+ } else {
+ if (prop != "isFirstRun") {
+ self._store[prop] = mapData[prop];
+ }
+ }
+ }
+
+ } else if (self.name == "action_map") {
+ for (let domain in mapData) {
+ let action = mapData[domain];
+
+ // Copy over any user settings from the merged-in data
+ if (action.userAction) {
+ if (self._store.hasOwnProperty(domain)) {
+ self._store[domain].userAction = action.userAction;
+ } else {
+ self._store[domain] = Object.assign(_newActionMapObject(), action);
+ }
+ }
+
+ // handle Do Not Track
+ if (self._store.hasOwnProperty(domain)) {
+ // Merge DNT settings if the imported data has a more recent update
+ if (action.nextUpdateTime > self._store[domain].nextUpdateTime) {
+ self._store[domain].nextUpdateTime = action.nextUpdateTime;
+ self._store[domain].dnt = action.dnt;
+ }
+ } else {
+ // Import action map entries for new DNT-compliant domains
+ if (action.dnt) {
+ self._store[domain] = Object.assign(_newActionMapObject(), action);
+ }
+ }
+ }
+
+ } else if (self.name == "snitch_map") {
+ for (let tracker_origin in mapData) {
+ let firstPartyOrigins = mapData[tracker_origin];
+ for (let i = 0; i < firstPartyOrigins.length; i++) {
+ badger.heuristicBlocking.updateTrackerPrevalence(
+ tracker_origin,
+ tracker_origin,
+ firstPartyOrigins[i]
+ );
+ }
+ }
+ }
+
+ // Async call to syncStorage.
+ setTimeout(function () {
+ _syncStorage(self);
+ }, 0);
+ }
+};
+
+var _syncStorage = (function () {
+ var debouncedFuncs = {};
+
+ function cb() {
+ if (chrome.runtime.lastError) {
+ let err = chrome.runtime.lastError.message;
+ if (!err.startsWith("IO error:") && !err.startsWith("Corruption:")
+ && !err.startsWith("InvalidStateError:") && !err.startsWith("AbortError:")
+ && !err.startsWith("QuotaExceededError:")
+ ) {
+ badger.criticalError = err;
+ }
+ console.error("Error writing to chrome.storage.local:", err);
+ }
+ }
+
+ function sync(badgerStorage) {
+ var obj = {};
+ obj[badgerStorage.name] = badgerStorage._store;
+ chrome.storage.local.set(obj, cb);
+ }
+
+ // Creates debounced versions of "sync" function,
+ // one for each distinct badgerStorage value.
+ return function (badgerStorage) {
+ if (!debouncedFuncs.hasOwnProperty(badgerStorage.name)) {
+ // call sync at most once every two seconds
+ debouncedFuncs[badgerStorage.name] = _.debounce(function () {
+ sync(badgerStorage);
+ }, 2000);
+ }
+ debouncedFuncs[badgerStorage.name]();
+ };
+}());
+
+/************************************** exports */
+var exports = {};
+
+exports.BadgerPen = BadgerPen;
+
+return exports;
+/************************************** exports */
+}());