/* * This file is part of Privacy Badger * 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 . */ /* 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 = { : { blockedFrameUrls: { : [ {String} blocked frame URL, ... ], ... }, fpData: { : { canvas: { fingerprinting: boolean, write: boolean } }, ... }, frames: { : { 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();