diff options
Diffstat (limited to '')
-rw-r--r-- | src/js/background.js | 1148 |
1 files changed, 1148 insertions, 0 deletions
diff --git a/src/js/background.js b/src/js/background.js new file mode 100644 index 0000000..6cdee4c --- /dev/null +++ b/src/js/background.js @@ -0,0 +1,1148 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2014 Electronic Frontier Foundation + * + * Derived from Adblock Plus + * Copyright (C) 2006-2013 Eyeo GmbH + * + * 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 log:false */ + +var utils = require("utils"); +var constants = require("constants"); +var pbStorage = require("storage"); + +var HeuristicBlocking = require("heuristicblocking"); +var FirefoxAndroid = require("firefoxandroid"); +var webrequest = require("webrequest"); +var widgetLoader = require("widgetloader"); + +var Migrations = require("migrations").Migrations; +var incognito = require("incognito"); + +/** + * Privacy Badger initializer. + */ +function Badger() { + let self = this; + + self.isFirstRun = false; + self.isUpdate = false; + + self.webRTCAvailable = checkWebRTCBrowserSupport(); + self.firstPartyDomainPotentiallyRequired = testCookiesFirstPartyDomain(); + + self.widgetList = []; + let widgetListPromise = widgetLoader.loadWidgetsFromFile( + "data/socialwidgets.json").catch(console.error); + widgetListPromise.then(widgets => { + self.widgetList = widgets; + }); + + self.storage = new pbStorage.BadgerPen(async function (thisStorage) { + self.initializeSettings(); + // Privacy Badger settings are now fully ready + + self.setPrivacyOverrides(); + + self.heuristicBlocking = new HeuristicBlocking.HeuristicBlocker(thisStorage); + + // TODO there are async migrations + // TODO is this the right place for migrations? + self.runMigrations(); + + // kick off async initialization steps + let seedDataPromise = self.loadFirstRunSeedData().catch(console.error), + ylistPromise = self.initializeYellowlist().catch(console.error), + dntHashesPromise = self.initializeDnt().catch(console.error), + tabDataPromise = self.updateTabList().catch(console.error); + + // set badge text color to white in Firefox 63+ + // https://bugzilla.mozilla.org/show_bug.cgi?id=1474110 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1424620 + if (chrome.browserAction.hasOwnProperty('setBadgeTextColor')) { + chrome.browserAction.setBadgeTextColor({ color: "#fff" }); + } + + // Show icon as page action for all tabs that already exist + chrome.tabs.query({}, function (tabs) { + for (let i = 0; i < tabs.length; i++) { + let tab = tabs[i]; + self.updateIcon(tab.id, tab.url); + } + }); + + // wait for async functions (seed data, yellowlist, ...) to resolve + await widgetListPromise; + await seedDataPromise; + await ylistPromise; + await dntHashesPromise; + await tabDataPromise; + + // block all widget domains + // only need to do this when the widget list could have gotten updated + if (badger.isFirstRun || badger.isUpdate) { + self.blockWidgetDomains(); + } + + // start the listeners + incognito.startListeners(); + webrequest.startListeners(); + HeuristicBlocking.startListeners(); + FirefoxAndroid.startListeners(); + startBackgroundListeners(); + + console.log("Privacy Badger is ready to rock!"); + console.log("Set DEBUG=1 to view console messages."); + self.INITIALIZED = true; + + // get the latest yellowlist from eff.org + self.updateYellowlist(err => { + if (err) { + console.error(err); + } + }); + // set up periodic fetching of the yellowlist from eff.org + setInterval(self.updateYellowlist.bind(self), utils.oneDay()); + + // get the latest DNT policy hashes from eff.org + self.updateDntPolicyHashes(err => { + if (err) { + console.error(err); + } + }); + // set up periodic fetching of hashes from eff.org + setInterval(self.updateDntPolicyHashes.bind(self), utils.oneDay() * 4); + + if (self.isFirstRun) { + self.showFirstRunPage(); + } + }); + + /** + * WebRTC availability check + */ + function checkWebRTCBrowserSupport() { + if (!(chrome.privacy && chrome.privacy.network && + chrome.privacy.network.webRTCIPHandlingPolicy)) { + return false; + } + + var available = true; + var connection = null; + + try { + var RTCPeerConnection = ( + window.RTCPeerConnection || window.webkitRTCPeerConnection + ); + if (RTCPeerConnection) { + connection = new RTCPeerConnection(null); + } + } catch (ex) { + available = false; + } + + if (connection !== null && connection.close) { + connection.close(); + } + + return available; + } + + /** + * Checks for availability of firstPartyDomain chrome.cookies API parameter. + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/getAll#Parameters + * + * firstPartyDomain is required when privacy.websites.firstPartyIsolate is enabled, + * and is in Firefox since Firefox 59. (firstPartyIsolate is in Firefox since 58). + * + * We don't care whether firstPartyIsolate is enabled, but rather whether + * firstPartyDomain is supported. Assuming firstPartyDomain is supported, + * setting it to null in chrome.cookies.getAll() produces the same result + * regardless of the state of firstPartyIsolate. + * + * firstPartyDomain is not currently supported in Chrome. + */ + function testCookiesFirstPartyDomain() { + try { + chrome.cookies.getAll({ + firstPartyDomain: null + }, function () {}); + } catch (ex) { + return false; + } + return true; + } + +} + +Badger.prototype = { + INITIALIZED: false, + + /** + * Per-tab data that gets cleaned up on tab closing looks like: + tabData = { + <tab_id>: { + blockedFrameUrls: { + <parent_frame_id>: [ + {String} blocked frame URL, + ... + ], + ... + }, + fpData: { + <script_origin>: { + canvas: { + fingerprinting: boolean, + write: boolean + } + }, + ... + }, + frames: { + <frame_id>: { + url: string, + host: string, + parent: int + }, + ... + }, + origins: { + domain.tld: {String} action taken for this domain + ... + } + }, + ... + } + */ + tabData: {}, + + + // Methods + + /** + * Sets various browser privacy overrides. + */ + setPrivacyOverrides: function () { + if (!chrome.privacy) { + return; + } + + let self = this; + + /** + * Sets a browser setting if Privacy Badger is allowed to set it. + */ + function _set_override(name, api, value) { + if (!api) { + return; + } + api.get({}, result => { + if (result.levelOfControl != "controllable_by_this_extension") { + return; + } + api.set({ + value, + scope: 'regular' + }, () => { + if (chrome.runtime.lastError) { + console.error("Privacy setting failed:", chrome.runtime.lastError); + } else { + console.log("Set", name, "to", value); + } + }); + }); + } + + if (self.getSettings().getItem("disableGoogleNavErrorService")) { + if (chrome.privacy.services) { + _set_override( + "alternateErrorPagesEnabled", + chrome.privacy.services.alternateErrorPagesEnabled, + false + ); + } + } + + if (self.getSettings().getItem("disableHyperlinkAuditing")) { + if (chrome.privacy.websites) { + _set_override( + "hyperlinkAuditingEnabled", + chrome.privacy.websites.hyperlinkAuditingEnabled, + false + ); + } + } + }, + + /** + * Loads seed dataset with pre-trained action and snitch maps. + * @param {Function} cb callback + */ + loadSeedData: function (cb) { + let self = this; + + utils.xhrRequest(constants.SEED_DATA_LOCAL_URL, function (err, response) { + if (err) { + return cb(new Error("Failed to fetch seed data")); + } + + let data; + try { + data = JSON.parse(response); + } catch (e) { + console.error(e); + return cb(new Error("Failed to parse seed data JSON")); + } + + self.mergeUserData(data, true); + log("Loaded seed data successfully"); + return cb(null); + }); + }, + + /** + * Loads seed data upon first run. + * + * @returns {Promise} + */ + loadFirstRunSeedData: function () { + let self = this; + + return new Promise(function (resolve, reject) { + if (!self.isFirstRun) { + log("No need to load seed data"); + return resolve(); + } + + self.loadSeedData(err => { + log("Seed data loaded! (err=%o)", err); + return (err ? reject(err) : resolve()); + }); + }); + }, + + showFirstRunPage: function() { + let settings = this.getSettings(); + if (settings.getItem("showIntroPage")) { + chrome.tabs.create({ + url: chrome.runtime.getURL("/skin/firstRun.html") + }); + } else { + // don't remind users to look at the intro page either + settings.setItem("seenComic", true); + } + }, + + /** + * Blocks all widget domains + * to ensure that all widgets that could get replaced + * do get replaced by default for all users. + */ + blockWidgetDomains: function () { + let self = this; + + // compile set of widget domains + let domains = new Set(); + for (let widget of self.widgetList) { + for (let domain of widget.domains) { + if (domain[0] == "*") { + domain = domain.slice(2); + } + domains.add(domain); + } + } + + // block the domains + for (let domain of domains) { + self.heuristicBlocking.blocklistOrigin( + window.getBaseDomain(domain), domain); + } + }, + + /** + * Saves a user preference for an origin, overriding the default setting. + * + * @param {String} userAction enum of block, cookieblock, noaction + * @param {String} origin the third party origin to take action on + */ + saveAction: function(userAction, origin) { + var allUserActions = { + block: constants.USER_BLOCK, + cookieblock: constants.USER_COOKIEBLOCK, + allow: constants.USER_ALLOW + }; + this.storage.setupUserAction(origin, allUserActions[userAction]); + log("Finished saving action " + userAction + " for " + origin); + }, + + /** + * Populate tabs object with currently open tabs when extension is updated or installed. + * + * @returns {Promise} + */ + updateTabList: function () { + let self = this; + + return new Promise(function (resolve) { + chrome.tabs.query({}, tabs => { + tabs.forEach(tab => { + // don't record on special browser pages + if (!utils.isRestrictedUrl(tab.url)) { + self.recordFrame(tab.id, 0, tab.url); + } + }); + resolve(); + }); + }); + }, + + /** + * Generate representation in internal data structure for frame + * + * @param {Integer} tabId ID of the tab + * @param {Integer} frameId ID of the frame + * @param {String} frameUrl The url of the frame + */ + recordFrame: function(tabId, frameId, frameUrl) { + let self = this; + + if (!self.tabData.hasOwnProperty(tabId)) { + self.tabData[tabId] = { + blockedFrameUrls: {}, + frames: {}, + origins: {} + }; + } + + self.tabData[tabId].frames[frameId] = { + url: frameUrl, + host: window.extractHostFromURL(frameUrl) + }; + }, + + /** + * Read the frame data from memory + * + * @param {Integer} tab_id Tab ID to check for + * @param {Integer} [frame_id=0] Frame ID to check for. + * Optional, defaults to frame 0 (the main document frame). + * + * @returns {?Object} Frame data object or null + */ + getFrameData: function (tab_id, frame_id) { + let self = this; + + frame_id = frame_id || 0; + + if (self.tabData.hasOwnProperty(tab_id)) { + if (self.tabData[tab_id].frames.hasOwnProperty(frame_id)) { + return self.tabData[tab_id].frames[frame_id]; + } + } + return null; + }, + + /** + * Initializes the yellowlist from disk. + * + * @returns {Promise} + */ + initializeYellowlist: function () { + let self = this; + + return new Promise(function (resolve, reject) { + + if (self.storage.getStore('cookieblock_list').keys().length) { + log("Yellowlist already initialized from disk"); + return resolve(); + } + + // we don't have the yellowlist initialized yet + // initialize from disk + utils.xhrRequest(constants.YELLOWLIST_LOCAL_URL, (error, response) => { + if (error) { + console.error(error); + return reject(new Error("Failed to fetch local yellowlist")); + } + + self.storage.updateYellowlist(response.trim().split("\n")); + log("Initialized ylist from disk"); + return resolve(); + }); + + }); + }, + + /** + * Updates to the latest yellowlist from eff.org. + * @param {Function} [callback] optional callback + */ + updateYellowlist: function (callback) { + let self = this; + + if (!callback) { + callback = function () {}; + } + + utils.xhrRequest(constants.YELLOWLIST_URL, function (err, response) { + if (err) { + console.error( + "Problem fetching yellowlist at", + constants.YELLOWLIST_URL, + err.status, + err.message + ); + + return callback(new Error("Failed to fetch remote yellowlist")); + } + + // handle empty response + if (!response.trim()) { + return callback(new Error("Empty yellowlist response")); + } + + let domains = response.trim().split("\n").map(domain => domain.trim()); + + // validate the response + if (!domains.every(domain => { + // all domains must contain at least one dot + if (domain.indexOf('.') == -1) { + return false; + } + + // validate character set + // + // regex says: + // - domain starts with lowercase English letter or Arabic numeral + // - following that, it contains one or more + // letter/numeral/dot/dash characters + // - following the previous two requirements, domain ends with a letter + // + // TODO both overly restrictive and inaccurate + // but that's OK for now, we manage the list + if (!/^[a-z0-9][a-z0-9.-]+[a-z]$/.test(domain)) { + return false; + } + + return true; + })) { + return callback(new Error("Invalid yellowlist response")); + } + + self.storage.updateYellowlist(domains); + log("Updated yellowlist from remote"); + + return callback(null); + }); + }, + + /** + * Initializes DNT policy hashes from disk. + * + * @returns {Promise} + */ + initializeDnt: function () { + let self = this; + + return new Promise(function (resolve, reject) { + + if (self.storage.getStore('dnt_hashes').keys().length) { + log("DNT hashes already initialized from disk"); + return resolve(); + } + + // we don't have DNT hashes initialized yet + // initialize from disk + utils.xhrRequest(constants.DNT_POLICIES_LOCAL_URL, (error, response) => { + let hashes; + + if (error) { + console.error(error); + return reject(new Error("Failed to fetch local DNT hashes")); + } + + try { + hashes = JSON.parse(response); + } catch (e) { + console.error(e); + return reject(new Error("Failed to parse DNT hashes JSON")); + } + + self.storage.updateDntHashes(hashes); + log("Initialized hashes from disk"); + return resolve(); + + }); + + }); + }, + + /** + * Fetch acceptable DNT policy hashes from the EFF server + * @param {Function} [cb] optional callback + */ + updateDntPolicyHashes: function (cb) { + let self = this; + + if (!cb) { + cb = function () {}; + } + + if (!self.isCheckingDNTPolicyEnabled()) { + // user has disabled this, we can check when they re-enable + setTimeout(function () { + return cb(null); + }, 0); + } + + utils.xhrRequest(constants.DNT_POLICIES_URL, function (err, response) { + if (err) { + console.error("Problem fetching DNT policy hash list at", + constants.DNT_POLICIES_URL, err.status, err.message); + return cb(new Error("Failed to fetch remote DNT hashes")); + } + + let hashes; + try { + hashes = JSON.parse(response); + } catch (e) { + console.error(e); + return cb(new Error("Failed to parse DNT hashes JSON")); + } + + self.storage.updateDntHashes(hashes); + log("Updated hashes from remote"); + return cb(null); + }); + }, + + /** + * Checks a domain for the EFF DNT policy. + * + * @param {String} domain The domain to check + * @param {Function} [cb] Callback that receives check status boolean (optional) + */ + checkForDNTPolicy: function (domain, cb) { + var self = this, + next_update = self.storage.getNextUpdateForDomain(domain); + + if (Date.now() < next_update) { + // not yet time + return; + } + + if (!self.isCheckingDNTPolicyEnabled()) { + // user has disabled this check + return; + } + + log('Checking', domain, 'for DNT policy.'); + + // update timestamp first; + // avoids queuing the same domain multiple times + var recheckTime = _.random( + utils.oneDayFromNow(), + utils.nDaysFromNow(7) + ); + self.storage.touchDNTRecheckTime(domain, recheckTime); + + self._checkPrivacyBadgerPolicy(domain, function (success) { + if (success) { + log('It looks like', domain, 'has adopted Do Not Track! I am going to unblock them'); + self.storage.setupDNT(domain); + } else { + log('It looks like', domain, 'has NOT adopted Do Not Track'); + self.storage.revertDNT(domain); + } + if (typeof cb == "function") { + cb(success); + } + }); + }, + + + /** + * Asyncronously checks if the domain has /.well-known/dnt-policy.txt. + * + * Rate-limited to at least one second apart. + * + * @param {String} origin The host to check + * @param {Function} callback callback(successStatus) + */ + _checkPrivacyBadgerPolicy: utils.rateLimit(function (origin, callback) { + var successStatus = false; + var url = "https://" + origin + "/.well-known/dnt-policy.txt"; + var dnt_hashes = this.storage.getStore('dnt_hashes'); + + utils.xhrRequest(url,function(err,response) { + if (err) { + callback(successStatus); + return; + } + utils.sha1(response, function(hash) { + if (dnt_hashes.hasItem(hash)) { + successStatus = true; + } + callback(successStatus); + }); + }); + }, constants.DNT_POLICY_CHECK_INTERVAL), + + /** + * Default Privacy Badger settings + */ + defaultSettings: { + checkForDNTPolicy: true, + disabledSites: [], + disableGoogleNavErrorService: true, + disableHyperlinkAuditing: true, + hideBlockedElements: true, + learnInIncognito: false, + learnLocally: false, + migrationLevel: 0, + seenComic: false, + sendDNTSignal: true, + showCounter: true, + showIntroPage: true, + showNonTrackingDomains: false, + showTrackingDomains: false, + socialWidgetReplacementEnabled: true, + widgetReplacementExceptions: [], + widgetSiteAllowlist: {}, + }, + + /** + * Initializes settings with defaults if needed, + * detects whether Badger just got installed or upgraded + */ + initializeSettings: function () { + let self = this, + settings = self.getSettings(); + + for (let key of Object.keys(self.defaultSettings)) { + // if this setting is not yet in storage, + if (!settings.hasItem(key)) { + // set with default value + let value = self.defaultSettings[key]; + log("setting", key, "=", value); + settings.setItem(key, value); + } + } + + let version = chrome.runtime.getManifest().version, + privateStore = self.getPrivateSettings(), + prev_version = privateStore.getItem("badgerVersion"); + + // special case for older badgers that kept isFirstRun in storage + if (settings.hasItem("isFirstRun")) { + self.isUpdate = true; + privateStore.setItem("badgerVersion", version); + privateStore.setItem("showLearningPrompt", true); + settings.deleteItem("isFirstRun"); + + // new install + } else if (!prev_version) { + self.isFirstRun = true; + privateStore.setItem("badgerVersion", version); + + // upgrade + } else if (version != prev_version) { + self.isUpdate = true; + privateStore.setItem("badgerVersion", version); + } + + if (!privateStore.hasItem("showLearningPrompt")) { + privateStore.setItem("showLearningPrompt", false); + } + }, + + runMigrations: function() { + var self = this; + var settings = self.getSettings(); + var migrationLevel = settings.getItem('migrationLevel'); + // TODO do not remove any migration methods + // TODO w/o refactoring migrationLevel handling to work differently + var migrations = [ + Migrations.changePrivacySettings, + Migrations.migrateAbpToStorage, + Migrations.migrateBlockedSubdomainsToCookieblock, + Migrations.migrateLegacyFirefoxData, + Migrations.migrateDntRecheckTimes, + // Need to run this migration again for everyone to #1181 + Migrations.migrateDntRecheckTimes2, + Migrations.forgetMistakenlyBlockedDomains, + Migrations.unblockIncorrectlyBlockedDomains, + Migrations.forgetBlockedDNTDomains, + Migrations.reapplyYellowlist, + Migrations.forgetNontrackingDomains, + Migrations.forgetMistakenlyBlockedDomains, + Migrations.resetWebRTCIPHandlingPolicy, + Migrations.enableShowNonTrackingDomains, + Migrations.forgetFirstPartySnitches, + Migrations.forgetCloudflare, + Migrations.forgetConsensu, + Migrations.resetWebRTCIPHandlingPolicy2, + ]; + + for (var i = migrationLevel; i < migrations.length; i++) { + migrations[i].call(Migrations, self); + settings.setItem('migrationLevel', i+1); + } + + }, + + /** + * Returns the count of tracking domains for a tab. + * @param {Integer} tab_id browser tab ID + * @returns {Integer} tracking domains count + */ + getTrackerCount: function (tab_id) { + let origins = this.tabData[tab_id].origins, + count = 0; + + for (let domain in origins) { + let action = origins[domain]; + if ( + action == constants.BLOCK || + action == constants.COOKIEBLOCK || + action == constants.USER_BLOCK || + action == constants.USER_COOKIEBLOCK + ) { + count++; + } + } + + return count; + }, + + /** + * Update page action badge with current count. + * @param {Integer} tab_id browser tab ID + */ + updateBadge: function (tab_id) { + if (!FirefoxAndroid.hasBadgeSupport) { + return; + } + + let self = this; + + chrome.tabs.get(tab_id, function (tab) { + if (chrome.runtime.lastError) { + // don't set on background (prerendered) tabs to avoid Chrome errors + return; + } + + if (!tab.active) { + // don't set on inactive tabs + return; + } + + if (self.criticalError) { + chrome.browserAction.setBadgeBackgroundColor({tabId: tab_id, color: "#cc0000"}); + chrome.browserAction.setBadgeText({tabId: tab_id, text: "!"}); + return; + } + + // don't show the counter for any of these: + // - the counter is disabled + // - we don't have tabData for whatever reason (special browser pages) + // - Privacy Badger is disabled on the page + if ( + !self.getSettings().getItem("showCounter") || + !self.tabData.hasOwnProperty(tab_id) || + !self.isPrivacyBadgerEnabled(self.getFrameData(tab_id).host) + ) { + chrome.browserAction.setBadgeText({tabId: tab_id, text: ""}); + return; + } + + let count = self.getTrackerCount(tab_id); + + if (count === 0) { + chrome.browserAction.setBadgeText({tabId: tab_id, text: ""}); + return; + } + + chrome.browserAction.setBadgeBackgroundColor({tabId: tab_id, color: "#ec9329"}); + chrome.browserAction.setBadgeText({tabId: tab_id, text: count + ""}); + }); + }, + + /** + * Shortcut helper for user-facing settings + */ + getSettings: function () { + return this.storage.getStore('settings_map'); + }, + + /** + * Shortcut helper for internal settings + */ + getPrivateSettings: function () { + return this.storage.getStore('private_storage'); + }, + + /** + * Check if privacy badger is enabled, take an origin and + * check against the disabledSites list + * + * @param {String} origin the origin to check + * @returns {Boolean} true if enabled + */ + isPrivacyBadgerEnabled: function(origin) { + var settings = this.getSettings(); + var disabledSites = settings.getItem("disabledSites"); + if (disabledSites && disabledSites.length > 0) { + for (var i = 0; i < disabledSites.length; i++) { + var site = disabledSites[i]; + + if (site.startsWith("*")) { + var wildcard = site.slice(1); // remove "*" + + if (origin.endsWith(wildcard)) { + return false; + } + } + + if (disabledSites[i] === origin) { + return false; + } + } + } + return true; + }, + + /** + * Is local learning generally enabled, + * and if tab_id is for an incognito window, + * is learning in incognito windows enabled? + */ + isLearningEnabled(tab_id) { + return ( + this.getSettings().getItem("learnLocally") && + incognito.learningEnabled(tab_id) + ); + }, + + /** + * Check if widget replacement functionality is enabled. + */ + isWidgetReplacementEnabled: function () { + return this.getSettings().getItem("socialWidgetReplacementEnabled"); + }, + + isDNTSignalEnabled: function() { + return this.getSettings().getItem("sendDNTSignal"); + }, + + isCheckingDNTPolicyEnabled: function() { + return this.getSettings().getItem("checkForDNTPolicy"); + }, + + /** + * Add an origin to the disabled sites list + * + * @param {String} origin The origin to disable the PB for + */ + disablePrivacyBadgerForOrigin: function(origin) { + var settings = this.getSettings(); + var disabledSites = settings.getItem('disabledSites'); + if (disabledSites.indexOf(origin) < 0) { + disabledSites.push(origin); + settings.setItem("disabledSites", disabledSites); + } + }, + + /** + * Returns the current list of disabled sites. + * + * @returns {Array} site domains where Privacy Badger is disabled + */ + getDisabledSites: function () { + return this.getSettings().getItem("disabledSites"); + }, + + /** + * Remove an origin from the disabledSites list + * + * @param {String} origin The origin to disable the PB for + */ + enablePrivacyBadgerForOrigin: function(origin) { + var settings = this.getSettings(); + var disabledSites = settings.getItem("disabledSites"); + var idx = disabledSites.indexOf(origin); + if (idx >= 0) { + disabledSites.splice(idx, 1); + settings.setItem("disabledSites", disabledSites); + } + }, + + /** + * Checks if local storage ( in dict) has any high-entropy keys + * + * @param {Object} lsItems Local storage dict + * @returns {boolean} true if it seems there are supercookies + */ + hasLocalStorageSupercookie: function (lsItems) { + var LOCALSTORAGE_ENTROPY_THRESHOLD = 33, // in bits + estimatedEntropy = 0, + lsKey = "", + lsItem = ""; + for (lsKey in lsItems) { + // send both key and value to entropy estimation + lsItem = lsItems[lsKey]; + log("Checking localstorage item", lsKey, lsItem); + estimatedEntropy += utils.estimateMaxEntropy(lsKey + lsItem); + if (estimatedEntropy > LOCALSTORAGE_ENTROPY_THRESHOLD) { + log("Found high-entropy localStorage: ", estimatedEntropy, + " bits, key: ", lsKey); + return true; + } + } + return false; + }, + + /** + * check if there seems to be any type of Super Cookie + * + * @param {Object} storageItems Dict with storage items + * @returns {Boolean} true if there seems to be any Super cookie + */ + hasSupercookie: function (storageItems) { + return ( + this.hasLocalStorageSupercookie(storageItems.localStorageItems) + //|| this.hasLocalStorageSupercookie(storageItems.indexedDBItems) + // TODO: See "Reading a directory's contents" on + // http://www.html5rocks.com/en/tutorials/file/filesystem/ + //|| this.hasLocalStorageSupercookie(storageItems.fileSystemAPIItems) + ); + }, + + /** + * Save third party origins to tabData[tab_id] object for + * use in the popup and, if needed, call updateBadge. + * + * @param {Integer} tab_id the tab we are on + * @param {String} fqdn the third party origin to add + * @param {String} action the action we are taking + */ + logThirdPartyOriginOnTab: function (tab_id, fqdn, action) { + let self = this, + is_blocked = ( + action == constants.BLOCK || + action == constants.COOKIEBLOCK || + action == constants.USER_BLOCK || + action == constants.USER_COOKIEBLOCK + ), + origins = self.tabData[tab_id].origins, + previously_blocked = origins.hasOwnProperty(fqdn) && ( + origins[fqdn] == constants.BLOCK || + origins[fqdn] == constants.COOKIEBLOCK || + origins[fqdn] == constants.USER_BLOCK || + origins[fqdn] == constants.USER_COOKIEBLOCK + ); + + origins[fqdn] = action; + + // no need to update badge if not a (cookie)blocked domain, + // or if we have already seen it as a (cookie)blocked domain + if (!is_blocked || previously_blocked) { + return; + } + + // don't block critical code paths on updating the badge + setTimeout(function () { + self.updateBadge(tab_id); + }, 0); + }, + + /** + * Enables or disables page action icon according to options. + * @param {Integer} tab_id The tab ID to set the badger icon for + * @param {String} tab_url The tab URL to set the badger icon for + */ + updateIcon: function (tab_id, tab_url) { + if (!tab_id || !tab_url || !FirefoxAndroid.hasPopupSupport) { + return; + } + + let self = this, iconFilename; + + // TODO grab hostname from tabData instead + if (!utils.isRestrictedUrl(tab_url) && + self.isPrivacyBadgerEnabled(window.extractHostFromURL(tab_url))) { + iconFilename = { + 19: chrome.runtime.getURL("icons/badger-19.png"), + 38: chrome.runtime.getURL("icons/badger-38.png") + }; + } else { + iconFilename = { + 19: chrome.runtime.getURL("icons/badger-19-disabled.png"), + 38: chrome.runtime.getURL("icons/badger-38-disabled.png") + }; + } + + chrome.browserAction.setIcon({tabId: tab_id, path: iconFilename}); + }, + + /** + * Merge data exported from a different badger into this badger's storage. + * + * @param {Object} data the user data to merge in + * @param {Boolean} [skip_migrations=false] set when running from a migration to avoid infinite loop + */ + mergeUserData: function (data, skip_migrations) { + let self = this; + // The order of these keys is also the order in which they should be imported. + // It's important that snitch_map be imported before action_map (#1972) + ["snitch_map", "action_map", "settings_map"].forEach(function (key) { + if (data.hasOwnProperty(key)) { + self.storage.getStore(key).merge(data[key]); + } + }); + + // for exports from older Privacy Badger versions: + // fix yellowlist getting out of sync, remove non-tracking domains, etc. + if (!skip_migrations) { + self.runMigrations(); + } + } + +}; + +/**************************** Listeners ****************************/ + +function startBackgroundListeners() { + chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { + if (changeInfo.status == "loading" && tab.url) { + badger.updateIcon(tab.id, tab.url); + badger.updateBadge(tabId); + } + }); + + // Update icon if a tab is replaced or loaded from cache + chrome.tabs.onReplaced.addListener(function(addedTabId/*, removedTabId*/) { + chrome.tabs.get(addedTabId, function(tab) { + badger.updateIcon(tab.id, tab.url); + }); + }); + + chrome.tabs.onActivated.addListener(function (activeInfo) { + badger.updateBadge(activeInfo.tabId); + }); +} + +var badger = window.badger = new Badger(); |