diff options
Diffstat (limited to 'src/js')
28 files changed, 12757 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(); diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js new file mode 100644 index 0000000..7d90ef4 --- /dev/null +++ b/src/js/bootstrap.js @@ -0,0 +1,37 @@ +/* + * 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/>. + */ + +window.DEBUG = false; +window.badger = {}; + +/** +* Log a message to the console if debugging is enabled +*/ +window.log = function (/*...*/) { + if (window.DEBUG) { + console.log.apply(console, arguments); + } +}; + +/** + * Basic implementation of requirejs + * for requiring other javascript files + */ +function require(module) { + return require.scopes[module]; +} +require.scopes = {}; diff --git a/src/js/constants.js b/src/js/constants.js new file mode 100644 index 0000000..5a77d02 --- /dev/null +++ b/src/js/constants.js @@ -0,0 +1,54 @@ +/* + * 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/>. + */ + +require.scopes.constants = (function() { + +var exports = { + + // Tracking status constants + NO_TRACKING: "noaction", + ALLOW: "allow", + BLOCK: "block", + COOKIEBLOCK: "cookieblock", + DNT: "dnt", + USER_ALLOW: "user_allow", + USER_BLOCK: "user_block", + USER_COOKIEBLOCK: "user_cookieblock", + + // URLS + DNT_POLICIES_URL: "https://www.eff.org/files/dnt-policies.json", + DNT_POLICIES_LOCAL_URL: chrome.runtime.getURL('data/dnt-policies.json'), + YELLOWLIST_URL: "https://www.eff.org/files/cookieblocklist_new.txt", + YELLOWLIST_LOCAL_URL: chrome.runtime.getURL('data/yellowlist.txt'), + SEED_DATA_LOCAL_URL: chrome.runtime.getURL('data/seed.json'), + + // The number of 1st parties a 3rd party can be seen on + TRACKING_THRESHOLD: 3, + MAX_COOKIE_ENTROPY: 12, + + DNT_POLICY_CHECK_INTERVAL: 1000, // one second +}; + +exports.BLOCKED_ACTIONS = new Set([ + exports.BLOCK, + exports.USER_BLOCK, + exports.COOKIEBLOCK, + exports.USER_COOKIEBLOCK, +]); + +return exports; +})(); diff --git a/src/js/contentscripts/clobbercookie.js b/src/js/contentscripts/clobbercookie.js new file mode 100644 index 0000000..402d00e --- /dev/null +++ b/src/js/contentscripts/clobbercookie.js @@ -0,0 +1,60 @@ +/* + * 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/>. + */ + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +// don't bother asking to run when trivially in first-party context +if (window.top == window) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkLocation", + frameUrl: window.FRAME_URL +}, function (blocked) { + if (blocked) { + var code = '('+ function() { + document.__defineSetter__("cookie", function(/*value*/) { }); + document.__defineGetter__("cookie", function() { return ""; }); + + // trim referrer down to origin + let referrer = document.referrer; + if (referrer) { + referrer = referrer.slice( + 0, + referrer.indexOf('/', referrer.indexOf('://') + 3) + ) + '/'; + } + document.__defineGetter__("referrer", function () { return referrer; }); + } +')();'; + + window.injectScript(code); + } + return true; +}); + +}()); diff --git a/src/js/contentscripts/clobberlocalstorage.js b/src/js/contentscripts/clobberlocalstorage.js new file mode 100644 index 0000000..7ff3528 --- /dev/null +++ b/src/js/contentscripts/clobberlocalstorage.js @@ -0,0 +1,94 @@ +/* + * 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/>. + */ + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +// don't bother asking to run when trivially in first-party context +if (window.top == window) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkLocation", + frameUrl: window.FRAME_URL +}, function (blocked) { + if (blocked) { + // https://stackoverflow.com/questions/49092423/how-to-break-on-localstorage-changes + var code = + '('+ function() { + + /* + * If localStorage is inaccessible, such as when "Block third-party cookies" + * in enabled in Chrome or when `dom.storage.enabled` is set to `false` in + * Firefox, do not go any further. + */ + try { + // No localStorage raises an Exception in Chromium-based browsers, while + // it's equal to `null` in Firefox. + if (null === localStorage) { + throw false; + } + } catch (ex) { + return; + } + + let lsProxy = new Proxy(localStorage, { + set: function (/*ls, prop, value*/) { + return true; + }, + get: function (ls, prop) { + if (typeof ls[prop] == 'function') { + let fn = function () {}; + if (prop == 'getItem' || prop == 'key') { + fn = function () { return null; }; + } + return fn.bind(ls); + } else { + if (prop == 'length') { + return 0; + } else if (prop == '__proto__') { + return lsProxy; + } + return; + } + } + }); + + Object.defineProperty(window, 'localStorage', { + configurable: true, + enumerable: true, + value: lsProxy + }); + + } +')()'; + + window.injectScript(code); + } + return true; +}); + +}()); diff --git a/src/js/contentscripts/collapser.js b/src/js/contentscripts/collapser.js new file mode 100644 index 0000000..968905c --- /dev/null +++ b/src/js/contentscripts/collapser.js @@ -0,0 +1,56 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2020 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/>. + */ + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +function hideFrame(url) { + let sel = "iframe[src='" + CSS.escape(url) + "']"; + let el = document.querySelector(sel); + if (el) { // el could have gotten replaced since the lookup + el.style.setProperty("display", "none", "important"); + } +} + +chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { + if (request.hideFrame) { + hideFrame(request.url); + sendResponse(true); + } +}); + +// check the page for any frames that were blocked before we got here +chrome.runtime.sendMessage({ + type: "getBlockedFrameUrls" +}, function (frameUrls) { + if (!frameUrls) { + return; + } + for (let url of frameUrls) { + hideFrame(url); + } +}); + +}()); diff --git a/src/js/contentscripts/dnt.js b/src/js/contentscripts/dnt.js new file mode 100644 index 0000000..0584748 --- /dev/null +++ b/src/js/contentscripts/dnt.js @@ -0,0 +1,66 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2018 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/>. + */ + +function getPageScript() { + + // code below is not a content script: no chrome.* APIs ///////////////////// + + // return a string + return "(" + function (NAVIGATOR, OBJECT) { + + OBJECT.defineProperty(OBJECT.getPrototypeOf(NAVIGATOR), "doNotTrack", { + get: function doNotTrack() { + return "1"; + } + }); + + OBJECT.defineProperty(OBJECT.getPrototypeOf(NAVIGATOR), "globalPrivacyControl", { + get: function globalPrivacyControl() { + return "1"; + } + }); + + // save locally to keep from getting overwritten by site code + } + "(window.navigator, Object));"; + + // code above is not a content script: no chrome.* APIs ///////////////////// + +} + +// END FUNCTION DEFINITIONS /////////////////////////////////////////////////// + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkDNT" +}, function (enabled) { + if (enabled) { + window.injectScript(getPageScript()); + } +}); + +}()); diff --git a/src/js/contentscripts/fingerprinting.js b/src/js/contentscripts/fingerprinting.js new file mode 100644 index 0000000..0891896 --- /dev/null +++ b/src/js/contentscripts/fingerprinting.js @@ -0,0 +1,367 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2015 Electronic Frontier Foundation + * + * Derived from Chameleon <https://github.com/ghostwords/chameleon> + * Copyright (C) 2015 ghostwords + * + * 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/>. + */ + +function getFpPageScript() { + + // code below is not a content script: no chrome.* APIs ///////////////////// + + // return a string + return "(" + function (DOCUMENT, dispatchEvent, CUSTOM_EVENT, ERROR, DATE, setTimeout, OBJECT) { + + const V8_STACK_TRACE_API = !!(ERROR && ERROR.captureStackTrace); + + if (V8_STACK_TRACE_API) { + ERROR.stackTraceLimit = Infinity; // collect all frames + } else { + // from https://github.com/csnover/TraceKit/blob/b76ad786f84ed0c94701c83d8963458a8da54d57/tracekit.js#L641 + var geckoCallSiteRe = /^\s*(.*?)(?:\((.*?)\))?@?((?:file|https?|chrome):.*?):(\d+)(?::(\d+))?\s*$/i; + } + + var event_id = DOCUMENT.currentScript.getAttribute('data-event-id'); + + // from Underscore v1.6.0 + function debounce(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function () { + var last = DATE.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function () { + context = this; // eslint-disable-line consistent-this + args = arguments; + timestamp = DATE.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + } + + // messages the injected script + var send = (function () { + var messages = []; + + // debounce sending queued messages + var _send = debounce(function () { + dispatchEvent.call(DOCUMENT, new CUSTOM_EVENT(event_id, { + detail: messages + })); + + // clear the queue + messages = []; + }, 100); + + return function (msg) { + // queue the message + messages.push(msg); + + _send(); + }; + }()); + + /** + * Gets the stack trace by throwing and catching an exception. + * @returns {*} Returns the stack trace + */ + function getStackTraceFirefox() { + let stack; + + try { + throw new ERROR(); + } catch (err) { + stack = err.stack; + } + + return stack.split('\n'); + } + + /** + * Gets the stack trace using the V8 stack trace API: + * https://github.com/v8/v8/wiki/Stack-Trace-API + * @returns {*} Returns the stack trace + */ + function getStackTrace() { + let err = {}, + origFormatter, + stack; + + origFormatter = ERROR.prepareStackTrace; + ERROR.prepareStackTrace = function (_, structuredStackTrace) { + return structuredStackTrace; + }; + + ERROR.captureStackTrace(err, getStackTrace); + stack = err.stack; + + ERROR.prepareStackTrace = origFormatter; + + return stack; + } + + /** + * Strip away the line and column number (from stack trace urls) + * @param script_url The stack trace url to strip + * @returns {String} the pure URL + */ + function stripLineAndColumnNumbers(script_url) { + return script_url.replace(/:\d+:\d+$/, ''); + } + + /** + * Parses the stack trace for the originating script URL + * without using the V8 stack trace API. + * @returns {String} The URL of the originating script + */ + function getOriginatingScriptUrlFirefox() { + let trace = getStackTraceFirefox(); + + if (trace.length < 4) { + return ''; + } + + // this script is at 0, 1 and 2 + let callSite = trace[3]; + + let scriptUrlMatches = callSite.match(geckoCallSiteRe); + return scriptUrlMatches && scriptUrlMatches[3] || ''; + } + + /** + * Parses the stack trace for the originating script URL. + * @returns {String} The URL of the originating script + */ + function getOriginatingScriptUrl() { + let trace = getStackTrace(); + + if (OBJECT.prototype.toString.call(trace) == '[object String]') { + // we failed to get a structured stack trace + trace = trace.split('\n'); + // this script is at 0, 1, 2 and 3 + let script_url_matches = trace[4].match(/\((http.*:\d+:\d+)/); + // TODO do we need stripLineAndColumnNumbers (in both places) here? + return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(trace[4]); + } + + if (trace.length < 2) { + return ''; + } + + // this script is at 0 and 1 + let callSite = trace[2]; + + if (callSite.isEval()) { + // argh, getEvalOrigin returns a string ... + let eval_origin = callSite.getEvalOrigin(), + script_url_matches = eval_origin.match(/\((http.*:\d+:\d+)/); + + // TODO do we need stripLineAndColumnNumbers (in both places) here? + return script_url_matches && stripLineAndColumnNumbers(script_url_matches[1]) || stripLineAndColumnNumbers(eval_origin); + } else { + return callSite.getFileName(); + } + } + + /** + * Monitor the writes in a canvas instance + * @param item special item objects + */ + function trapInstanceMethod(item) { + var is_canvas_write = ( + item.propName == 'fillText' || item.propName == 'strokeText' + ); + + item.obj[item.propName] = (function (orig) { + // set to true after the first write, if the method is not + // restorable. Happens if another library also overwrites + // this method. + var skip_monitoring = false; + + function wrapped() { + var args = arguments; + + if (is_canvas_write) { + // to avoid false positives, + // bail if the text being written is too short, + // of if we've already sent a monitoring payload + if (skip_monitoring || !args[0] || args[0].length < 5) { + return orig.apply(this, args); + } + } + + var script_url = ( + V8_STACK_TRACE_API ? + getOriginatingScriptUrl() : + getOriginatingScriptUrlFirefox() + ), + msg = { + obj: item.objName, + prop: item.propName, + scriptUrl: script_url + }; + + if (item.hasOwnProperty('extra')) { + msg.extra = item.extra.apply(this, args); + } + + send(msg); + + if (is_canvas_write) { + // optimization: one canvas write is enough, + // restore original write method + // to this CanvasRenderingContext2D object instance + // Careful! Only restorable if we haven't already been replaced + // by another lib, such as the hidpi polyfill + if (this[item.propName] === wrapped) { + this[item.propName] = orig; + } else { + skip_monitoring = true; + } + } + + return orig.apply(this, args); + } + + OBJECT.defineProperty(wrapped, "name", { value: orig.name }); + OBJECT.defineProperty(wrapped, "length", { value: orig.length }); + OBJECT.defineProperty(wrapped, "toString", { value: orig.toString.bind(orig) }); + + return wrapped; + + }(item.obj[item.propName])); + } + + var methods = []; + + ['getImageData', 'fillText', 'strokeText'].forEach(function (method) { + var item = { + objName: 'CanvasRenderingContext2D.prototype', + propName: method, + obj: CanvasRenderingContext2D.prototype, + extra: function () { + return { + canvas: true + }; + } + }; + + if (method == 'getImageData') { + item.extra = function () { + var args = arguments, + width = args[2], + height = args[3]; + + // "this" is a CanvasRenderingContext2D object + if (width === undefined) { + width = this.canvas.width; + } + if (height === undefined) { + height = this.canvas.height; + } + + return { + canvas: true, + width: width, + height: height + }; + }; + } + + methods.push(item); + }); + + methods.push({ + objName: 'HTMLCanvasElement.prototype', + propName: 'toDataURL', + obj: HTMLCanvasElement.prototype, + extra: function () { + // "this" is a canvas element + return { + canvas: true, + width: this.width, + height: this.height + }; + } + }); + + methods.forEach(trapInstanceMethod); + + // save locally to keep from getting overwritten by site code + } + "(document, document.dispatchEvent, CustomEvent, Error, Date, setTimeout, Object));"; + + // code above is not a content script: no chrome.* APIs ///////////////////// + +} + +// END FUNCTION DEFINITIONS /////////////////////////////////////////////////// + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "detectFingerprinting" +}, function (enabled) { + if (!enabled) { + return; + } + /** + * Communicating to webrequest.js + */ + var event_id = Math.random(); + + // listen for messages from the script we are about to insert + document.addEventListener(event_id, function (e) { + // pass these on to the background page + chrome.runtime.sendMessage({ + type: "fpReport", + data: e.detail + }); + }); + + window.injectScript(getFpPageScript(), { + event_id: event_id + }); +}); + +}()); diff --git a/src/js/contentscripts/socialwidgets.js b/src/js/contentscripts/socialwidgets.js new file mode 100644 index 0000000..14ae2b3 --- /dev/null +++ b/src/js/contentscripts/socialwidgets.js @@ -0,0 +1,641 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2014 Electronic Frontier Foundation + * Derived from ShareMeNot + * Copyright (C) 2011-2014 University of Washington + * + * 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/>. + */ + +/* + * ShareMeNot is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright (c) 2011-2014 University of Washington + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +// widget data +let widgetList; + +// cached chrome.i18n.getMessage() results +const TRANSLATIONS = {}; + +// references to widget page elements +const WIDGET_ELS = {}; + + +/** + * @param {Object} response response to checkWidgetReplacementEnabled + */ +function initialize(response) { + for (const key in response.translations) { + TRANSLATIONS[key] = response.translations[key]; + } + + widgetList = response.widgetList; + + // check for widgets blocked before we got here + replaceInitialTrackerButtonsHelper(response.widgetsToReplace); + + // set up listener for dynamically created widgets + chrome.runtime.onMessage.addListener(function (request) { + if (request.replaceWidget) { + replaceSubsequentTrackerButtonsHelper(request.trackerDomain); + } + }); +} + +/** + * Creates a replacement placeholder element for the given widget. + * + * @param {Object} widget the SocialWidget object + * @param {Element} trackerElem the button/widget element we are replacing + * @param {Function} callback called with the replacement element + */ +function createReplacementElement(widget, trackerElem, callback) { + let buttonData = widget.replacementButton; + + // no image data to fetch + if (!buttonData.hasOwnProperty('imagePath')) { + return setTimeout(function () { + _createReplacementElementCallback(widget, trackerElem, callback); + }, 0); + } + + // already have replacement button image URI cached + if (buttonData.buttonUrl) { + return setTimeout(function () { + _createReplacementElementCallback(widget, trackerElem, callback); + }, 0); + } + + // already messaged for but haven't yet received the image data + if (buttonData.loading) { + // check back in 10 ms + return setTimeout(function () { + createReplacementElement(widget, trackerElem, callback); + }, 10); + } + + // don't have image data cached yet, get it from the background page + buttonData.loading = true; + chrome.runtime.sendMessage({ + type: "getReplacementButton", + widgetName: widget.name + }, function (response) { + if (response) { + buttonData.buttonUrl = response; // cache image data + _createReplacementElementCallback(widget, trackerElem, callback); + } + }); +} + +function _createReplacementElementCallback(widget, trackerElem, callback) { + if (widget.replacementButton.buttonUrl) { + _createButtonReplacement(widget, callback); + } else { + _createWidgetReplacement(widget, trackerElem, callback); + } +} + +function _createButtonReplacement(widget, callback) { + let buttonData = widget.replacementButton, + button_type = buttonData.type; + + let button = document.createElement("img"); + button.setAttribute("src", buttonData.buttonUrl); + + // TODO use custom tooltip to support RTL locales? + button.setAttribute( + "title", + TRANSLATIONS.social_tooltip_pb_has_replaced.replace("XXX", widget.name) + ); + + let styleAttrs = [ + "border: none", + "cursor: pointer", + "height: auto", + "width: auto", + ]; + button.setAttribute("style", styleAttrs.join(" !important;") + " !important"); + + // normal button type; just open a new window when clicked + if (button_type === 0) { + let popup_url = buttonData.details + encodeURIComponent(window.location.href); + + button.addEventListener("click", function () { + window.open(popup_url); + }); + + // in place button type; replace the existing button + // with an iframe when clicked + } else if (button_type == 1) { + let iframe_url = buttonData.details + encodeURIComponent(window.location.href); + + button.addEventListener("click", function () { + replaceButtonWithIframeAndUnblockTracker(button, widget.name, iframe_url); + }, { once: true }); + + // in place button type; replace the existing button with code + // specified in the widgets JSON + } else if (button_type == 2) { + button.addEventListener("click", function () { + replaceButtonWithHtmlCodeAndUnblockTracker(button, widget.name, buttonData.details); + }, { once: true }); + } + + callback(button); +} + +function _createWidgetReplacement(widget, trackerElem, callback) { + let replacementEl; + + // in-place widget type: + // reinitialize the widget by reinserting its element's HTML + if (widget.replacementButton.type == 3) { + replacementEl = createReplacementWidget( + widget, trackerElem, reinitializeWidgetAndUnblockTracker); + + // in-place widget type: + // reinitialize the widget by reinserting its element's HTML + // and activating associated scripts + } else if (widget.replacementButton.type == 4) { + let activationFn = replaceWidgetAndReloadScripts; + + // if there are no matching script elements + if (!document.querySelectorAll(widget.scriptSelectors.join(',')).length) { + // and we don't have a fallback script URL + if (!widget.fallbackScriptUrl) { + // we can't do "in-place" activation; reload the page instead + activationFn = function () { + unblockTracker(widget.name, function () { + location.reload(); + }); + }; + } + } + + replacementEl = createReplacementWidget(widget, trackerElem, activationFn); + } + + callback(replacementEl); +} + +/** + * Unblocks the given widget and replaces the given button with an iframe + * pointing to the given URL. + * + * @param {Element} button the DOM element of the button to replace + * @param {String} widget_name the name of the replacement widget + * @param {String} iframeUrl the URL of the iframe to replace the button + */ +function replaceButtonWithIframeAndUnblockTracker(button, widget_name, iframeUrl) { + unblockTracker(widget_name, function () { + // check is needed as for an unknown reason this callback function is + // executed for buttons that have already been removed; we are trying + // to prevent replacing an already removed button + if (button.parentNode !== null) { + let iframe = document.createElement("iframe"); + + iframe.setAttribute("src", iframeUrl); + iframe.setAttribute("style", "border: none !important; height: 1.5em !important;"); + + button.parentNode.replaceChild(iframe, button); + } + }); +} + +/** + * Unblocks the given widget and replaces the given button with the + * HTML code defined in the provided SocialWidget object. + * + * @param {Element} button the DOM element of the button to replace + * @param {String} widget_name the name of the replacement widget + * @param {String} html the HTML string that should replace the button + */ +function replaceButtonWithHtmlCodeAndUnblockTracker(button, widget_name, html) { + unblockTracker(widget_name, function () { + // check is needed as for an unknown reason this callback function is + // executed for buttons that have already been removed; we are trying + // to prevent replacing an already removed button + if (button.parentNode !== null) { + let codeContainer = document.createElement("div"); + codeContainer.innerHTML = html; + + button.parentNode.replaceChild(codeContainer, button); + + replaceScriptsRecurse(codeContainer); + } + }); +} + +/** + * Unblocks the given widget and replaces our replacement placeholder + * with the original third-party widget element. + * + * The teardown to the initialization defined in createReplacementWidget(). + * + * @param {String} name the name/type of this widget (SoundCloud, Vimeo etc.) + */ +function reinitializeWidgetAndUnblockTracker(name) { + unblockTracker(name, function () { + // restore all widgets of this type + WIDGET_ELS[name].forEach(data => { + data.parent.replaceChild(data.widget, data.replacement); + }); + WIDGET_ELS[name] = []; + }); +} + +/** + * Similar to reinitializeWidgetAndUnblockTracker() above, + * but also reruns scripts defined in scriptSelectors. + * + * @param {String} name the name/type of this widget (Disqus, Google reCAPTCHA) + */ +function replaceWidgetAndReloadScripts(name) { + unblockTracker(name, function () { + // restore all widgets of this type + WIDGET_ELS[name].forEach(data => { + data.parent.replaceChild(data.widget, data.replacement); + reloadScripts(data.scriptSelectors, data.fallbackScriptUrl); + }); + WIDGET_ELS[name] = []; + }); +} + +/** + * Find and replace script elements with their copies to trigger re-running. + */ +function reloadScripts(selectors, fallback_script_url) { + let scripts = document.querySelectorAll(selectors.join(',')); + + // if there are no matches, try a known script URL + if (!scripts.length && fallback_script_url) { + let parent = document.documentElement, + replacement = document.createElement("script"); + replacement.src = fallback_script_url; + parent.insertBefore(replacement, parent.firstChild); + return; + } + + for (let scriptEl of scripts) { + // reinsert script elements only + if (!scriptEl.nodeName || scriptEl.nodeName.toLowerCase() != 'script') { + continue; + } + + let replacement = document.createElement("script"); + for (let attr of scriptEl.attributes) { + replacement.setAttribute(attr.nodeName, attr.value); + } + scriptEl.parentNode.replaceChild(replacement, scriptEl); + // reinsert one script and quit + break; + } +} + +/** + * Dumping scripts into innerHTML won't execute them, so replace them + * with executable scripts. + */ +function replaceScriptsRecurse(node) { + if (node.nodeName && node.nodeName.toLowerCase() == 'script' && + node.getAttribute && node.getAttribute("type") == "text/javascript") { + let script = document.createElement("script"); + script.text = node.innerHTML; + script.src = node.src; + node.parentNode.replaceChild(script, node); + } else { + let i = 0, + children = node.childNodes; + while (i < children.length) { + replaceScriptsRecurse(children[i]); + i++; + } + } + return node; +} + + +/** + * Replaces all tracker buttons on the current web page with the internal + * replacement buttons, respecting the user's blocking settings. + * + * @param {Array} widgetsToReplace a list of widget names to replace + */ +function replaceInitialTrackerButtonsHelper(widgetsToReplace) { + widgetList.forEach(function (widget) { + if (widgetsToReplace.hasOwnProperty(widget.name)) { + replaceIndividualButton(widget); + } + }); +} + +/** + * Individually replaces tracker buttons blocked after initial check. + */ +function replaceSubsequentTrackerButtonsHelper(tracker_domain) { + if (!widgetList) { + return; + } + widgetList.forEach(function (widget) { + let replace = widget.domains.some(domain => { + if (domain == tracker_domain) { + return true; + // leading wildcard + } else if (domain[0] == "*") { + if (tracker_domain.endsWith(domain.slice(1))) { + return true; + } + } + return false; + }); + if (replace) { + replaceIndividualButton(widget); + } + }); +} + +function _make_id(prefix) { + return prefix + "-" + Math.random().toString().replace(".", ""); +} + +function createReplacementWidget(widget, elToReplace, activationFn) { + let name = widget.name; + + let widgetFrame = document.createElement('iframe'); + + // widget replacement frame styles + let border_width = 1; + let styleAttrs = [ + "background-color: #fff", + "border: " + border_width + "px solid #ec9329", + "min-width: 220px", + "min-height: 210px", + "max-height: 600px", + "z-index: 2147483647", + ]; + if (elToReplace.offsetWidth > 0) { + styleAttrs.push(`width: ${elToReplace.offsetWidth - 2*border_width}px`); + } + if (elToReplace.offsetHeight > 0) { + styleAttrs.push(`height: ${elToReplace.offsetHeight - 2*border_width}px`); + } + widgetFrame.style = styleAttrs.join(" !important;") + " !important"; + + let widgetDiv = document.createElement('div'); + + // parent div styles + styleAttrs = [ + "display: flex", + "flex-direction: column", + "align-items: center", + "justify-content: center", + "width: 100%", + "height: 100%", + ]; + if (TRANSLATIONS.rtl) { + styleAttrs.push("direction: rtl"); + } + widgetDiv.style = styleAttrs.join(" !important;") + " !important"; + + // child div styles + styleAttrs = [ + "color: #303030", + "font-family: helvetica, arial, sans-serif", + "font-size: 16px", + "display: flex", + "flex-wrap: wrap", + "justify-content: center", + "text-align: center", + "margin: 10px", + ]; + + let textDiv = document.createElement('div'); + textDiv.style = styleAttrs.join(" !important;") + " !important"; + textDiv.appendChild(document.createTextNode( + TRANSLATIONS.widget_placeholder_pb_has_replaced.replace("XXX", name))); + let infoIcon = document.createElement('a'), + info_icon_id = _make_id("ico-help"); + infoIcon.id = info_icon_id; + infoIcon.href = "https://privacybadger.org/#How-does-Privacy-Badger-handle-social-media-widgets"; + infoIcon.rel = "noreferrer"; + infoIcon.target = "_blank"; + textDiv.appendChild(infoIcon); + widgetDiv.appendChild(textDiv); + + let buttonDiv = document.createElement('div'); + styleAttrs.push("width: 100%"); + buttonDiv.style = styleAttrs.join(" !important;") + " !important"; + + // allow once button + let button = document.createElement('button'), + button_id = _make_id("btn-once"); + button.id = button_id; + styleAttrs = [ + "transition: background-color 0.25s ease-out, border-color 0.25s ease-out, color 0.25s ease-out", + "border-radius: 3px", + "cursor: pointer", + "font-family: 'Lucida Grande', 'Segoe UI', Tahoma, 'DejaVu Sans', Arial, sans-serif", + "font-size: 12px", + "font-weight: bold", + "line-height: 16px", + "padding: 10px", + "margin: 4px", + "text-align: center", + "width: 70%", + "max-width: 280px", + ]; + button.style = styleAttrs.join(" !important;") + " !important"; + + // allow on this site button + let site_button = document.createElement('button'), + site_button_id = _make_id("btn-site"); + site_button.id = site_button_id; + site_button.style = styleAttrs.join(" !important;") + " !important"; + + button.appendChild(document.createTextNode(TRANSLATIONS.allow_once)); + site_button.appendChild(document.createTextNode(TRANSLATIONS.allow_on_site)); + + buttonDiv.appendChild(button); + buttonDiv.appendChild(site_button); + + widgetDiv.appendChild(buttonDiv); + + // save refs. to elements for use in teardown + if (!WIDGET_ELS.hasOwnProperty(name)) { + WIDGET_ELS[name] = []; + } + let data = { + parent: elToReplace.parentNode, + widget: elToReplace, + replacement: widgetFrame + }; + if (widget.scriptSelectors) { + data.scriptSelectors = widget.scriptSelectors; + if (widget.fallbackScriptUrl) { + data.fallbackScriptUrl = widget.fallbackScriptUrl; + } + } + WIDGET_ELS[name].push(data); + + // set up click handler + widgetFrame.addEventListener('load', function () { + let onceButton = widgetFrame.contentDocument.getElementById(button_id), + siteButton = widgetFrame.contentDocument.getElementById(site_button_id); + + onceButton.addEventListener("click", function (e) { + e.preventDefault(); + activationFn(name); + }, { once: true }); + + siteButton.addEventListener("click", function (e) { + e.preventDefault(); + + // first message the background page to record that + // this widget should always be allowed on this site + chrome.runtime.sendMessage({ + type: "allowWidgetOnSite", + widgetName: name + }, function () { + activationFn(name); + }); + }, { once: true }); + + }, false); // end of click handler + + let head_styles = ` +html, body { + height: 100% !important; + overflow: hidden !important; +} +#${button_id} { + border: 2px solid #f06a0a !important; + background-color: #f06a0a !important; + color: #fefefe !important; +} +#${site_button_id} { + border: 2px solid #333 !important; + background-color: #fefefe !important; + color: #333 !important; +} +#${button_id}:hover { + background-color: #fefefe !important; + color: #333 !important; +} +#${site_button_id}:hover { + background-color: #fefefe !important; + border: 2px solid #f06a0a !important; +} +#${info_icon_id} { + position: absolute; + ${TRANSLATIONS.rtl ? "left" : "right"}: 4px; + top: 4px; + line-height: 12px; + text-decoration: none; +} +#${info_icon_id}:before { + border: 2px solid; + border-radius: 50%; + display: inline-block; + color: #555; + content: '?'; + font-size: 12px; + font-weight: bold; + padding: 1px; + height: 1em; + width: 1em; +} +#${info_icon_id}:hover:before { + color: #ec9329; +} + `.trim(); + + widgetFrame.srcdoc = '<html><head><style>' + head_styles + '</style></head><body style="margin:0">' + widgetDiv.outerHTML + '</body></html>'; + + return widgetFrame; +} + +/** + * Replaces buttons/widgets in the DOM. + */ +function replaceIndividualButton(widget) { + let selector = widget.buttonSelectors.join(','), + elsToReplace = document.querySelectorAll(selector); + + elsToReplace.forEach(function (el) { + createReplacementElement(widget, el, function (replacementEl) { + el.parentNode.replaceChild(replacementEl, el); + }); + }); +} + +/** + * Messages the background page to temporarily allow domains associated with a + * given replacement widget. + * Calls the provided callback function upon response. + * + * @param {String} name the name of the replacement widget + * @param {Function} callback the callback function + */ +function unblockTracker(name, callback) { + let request = { + type: "unblockWidget", + widgetName: name + }; + chrome.runtime.sendMessage(request, callback); +} + +// END FUNCTION DEFINITIONS /////////////////////////////////////////////////// + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +chrome.runtime.sendMessage({ + type: "checkWidgetReplacementEnabled" +}, function (response) { + if (!response) { + return; + } + initialize(response); +}); + +}()); diff --git a/src/js/contentscripts/supercookie.js b/src/js/contentscripts/supercookie.js new file mode 100644 index 0000000..0b15211 --- /dev/null +++ b/src/js/contentscripts/supercookie.js @@ -0,0 +1,151 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2015 Electronic Frontier Foundation + * + * Derived from Chameleon <https://github.com/ghostwords/chameleon> + * Copyright (C) 2015 ghostwords + * + * 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/>. + */ + +/** + * Generate script to inject into the page + * + * @returns {string} + */ +function getScPageScript() { + // code below is not a content script: no chrome.* APIs ///////////////////// + + // return a string + return "(" + function () { + + /* + * If localStorage is inaccessible, such as when "Block third-party cookies" + * in enabled in Chrome or when `dom.storage.enabled` is set to `false` in + * Firefox, do not go any further. + */ + try { + // No localStorage raises an Exception in Chromium-based browsers, while + // it's equal to `null` in Firefox. + if (null === localStorage) { + throw false; + } + } catch (ex) { + return; + } + + (function (DOCUMENT, dispatchEvent, CUSTOM_EVENT, LOCAL_STORAGE, OBJECT, keys) { + + var event_id = DOCUMENT.currentScript.getAttribute('data-event-id-super-cookie'); + + /** + * send message to the content script + * + * @param {*} message + */ + var send = function (message) { + dispatchEvent.call(DOCUMENT, new CUSTOM_EVENT(event_id, { + detail: message + })); + }; + + /** + * Read HTML5 local storage and return contents + * @returns {Object} + */ + let getLocalStorageItems = function () { + let lsItems = {}; + for (let i = 0; i < LOCAL_STORAGE.length; i++) { + let key = LOCAL_STORAGE.key(i); + lsItems[key] = LOCAL_STORAGE.getItem(key); + } + return lsItems; + }; + + if (event_id) { // inserted script may run before the event_id is available + let localStorageItems = getLocalStorageItems(); + if (keys.call(OBJECT, localStorageItems).length) { + // send to content script + send({ localStorageItems }); + } + } + + // save locally to keep from getting overwritten by site code + } (document, document.dispatchEvent, CustomEvent, localStorage, Object, Object.keys)); + + } + "());"; + + // code above is not a content script: no chrome.* APIs ///////////////////// + +} + +// END FUNCTION DEFINITIONS /////////////////////////////////////////////////// + +(function () { + +// don't inject into non-HTML documents (such as XML documents) +// but do inject into XHTML documents +if (document instanceof HTMLDocument === false && ( + document instanceof XMLDocument === false || + document.createElement('div') instanceof HTMLDivElement === false +)) { + return; +} + +// don't bother asking to run when trivially in first-party context +if (window.top == window) { + return; +} + +// TODO race condition; fix waiting on https://crbug.com/478183 + +// TODO here we could also be injected too quickly +// and miss localStorage setting upon initial page load +// +// we should eventually switch injection back to document_start +// (reverting https://github.com/EFForg/privacybadger/pull/1522), +// and fix localstorage detection +// (such as by delaying it or peforming it periodically) +// +// could then remove test workarounds like +// https://github.com/EFForg/privacybadger/commit/39d5d0899e22d1c451d429e44553c5f9cad7fc46 + +// TODO sometimes contentscripts/utils.js isn't here?! +// TODO window.FRAME_URL / window.injectScript are undefined ... +chrome.runtime.sendMessage({ + type: "inspectLocalStorage", + frameUrl: window.FRAME_URL +}, function (enabledAndThirdParty) { + if (!enabledAndThirdParty) { + return; + } + + var event_id_super_cookie = Math.random(); + + // listen for messages from the script we are about to insert + document.addEventListener(event_id_super_cookie, function (e) { + // pass these on to the background page (handled by webrequest.js) + chrome.runtime.sendMessage({ + type: "supercookieReport", + data: e.detail, + frameUrl: window.FRAME_URL + }); + }); + + window.injectScript(getScPageScript(), { + event_id_super_cookie: event_id_super_cookie + }); + +}); + +}()); diff --git a/src/js/contentscripts/utils.js b/src/js/contentscripts/utils.js new file mode 100644 index 0000000..9f0a0fa --- /dev/null +++ b/src/js/contentscripts/utils.js @@ -0,0 +1,53 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2018 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/>. + */ + +/** + * Executes a script in the page's JavaScript context. + * + * @param {String} text The content of the script to insert. + * @param {Object} data Data attributes to set on the inserted script tag. + */ +window.injectScript = function (text, data) { + var parent = document.documentElement, + script = document.createElement('script'); + + script.text = text; + script.async = false; + + for (var key in data) { + script.setAttribute('data-' + key.replace(/_/g, '-'), data[key]); + } + + parent.insertBefore(script, parent.firstChild); + parent.removeChild(script); +}; + +function getFrameUrl() { + let url = document.location.href, + parentFrame = (document != window.top) && window.parent; + while (parentFrame && url && !url.startsWith("http")) { + try { + url = parentFrame.document.location.href; + } catch (ex) { + // ignore 'Blocked a frame with origin "..." + // from accessing a cross-origin frame.' exceptions + } + parentFrame = (parentFrame != window.top) && parentFrame.parent; + } + return url; +} +window.FRAME_URL = getFrameUrl(); diff --git a/src/js/firefoxandroid.js b/src/js/firefoxandroid.js new file mode 100644 index 0000000..07eeb6a --- /dev/null +++ b/src/js/firefoxandroid.js @@ -0,0 +1,90 @@ +/* + * 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/>. + */ + +/* + * Temporary polyfill for firefox android, + * while it doesn't support the full browserAction API + * Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1330159 + */ + +require.scopes.firefoxandroid = (function() { +var hasPopupSupport = !!( + chrome.browserAction.setPopup && + chrome.browserAction.getPopup +); +var hasBadgeSupport = !!chrome.browserAction.setBadgeText; + +// keeps track of popup id while one is open +var openPopupId = false; +var popup_url = chrome.runtime.getManifest().browser_action.default_popup; + +// fakes a popup +function openPopup() { + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { + var url = popup_url + "?tabId=" + tabs[0].id; + chrome.tabs.create({url, index: tabs[0].index + 1}, (tab) => { + openPopupId = tab.id; + }); + }); +} + +// remove the 'popup' when another tab is activated +function onActivated(activeInfo) { + if (openPopupId != false && openPopupId != activeInfo.tabId) { + chrome.tabs.remove(openPopupId, () => { + openPopupId = false; + }); + } +} + +// forgets the popup when the url is overwritten by the user +function onUpdated(tabId, changeInfo, tab) { + if (tab.url && openPopupId == tabId) { + var new_url = new URL(tab.url); + + if (new_url.origin + new_url.pathname != popup_url) { + openPopupId = false; + } + } +} + +// Subscribe to events needed to fake a popup +function startListeners() { + if (!hasPopupSupport) { + chrome.browserAction.onClicked.addListener(openPopup); + chrome.tabs.onActivated.addListener(onActivated); + chrome.tabs.onUpdated.addListener(onUpdated); + } +} + +// Used in popup.js, figures out which tab opened the 'fake' popup +function getParentOfPopup(callback) { + chrome.tabs.query({active: true, currentWindow: true}, function(focusedTab) { + var parentId = parseInt(new URL(focusedTab[0].url).searchParams.get('tabId')); + chrome.tabs.get(parentId, callback); + }); +} + +/************************************** exports */ +var exports = {}; +exports.startListeners = startListeners; +exports.hasPopupSupport = hasPopupSupport; +exports.hasBadgeSupport = hasBadgeSupport; +exports.getParentOfPopup = getParentOfPopup; +return exports; +/************************************** exports */ +})(); diff --git a/src/js/firstparties/facebook.js b/src/js/firstparties/facebook.js new file mode 100644 index 0000000..2c5d299 --- /dev/null +++ b/src/js/firstparties/facebook.js @@ -0,0 +1,56 @@ +/* globals findInAllFrames:false, observeMutations:false */ +// Adapted from https://github.com/mgziminsky/FacebookTrackingRemoval +// this should only run on facebook.com, messenger.com, and +// facebookcorewwwi.onion +let fb_wrapped_link = `a[href*='${document.domain.split(".").slice(-2).join(".")}/l.php?']:not([aria-label])`; + +// remove all attributes from a link except for class and ARIA attributes +function cleanAttrs(elem) { + for (let i = elem.attributes.length - 1; i >= 0; --i) { + const attr = elem.attributes[i]; + if (attr.name !== 'class' && !attr.name.startsWith('aria-')) { + elem.removeAttribute(attr.name); + } + } +} + +// Remove excessive attributes and event listeners from link a and replace +// its destination with href +function cleanLink(a) { + let href = new URL(a.href).searchParams.get('u'); + + // If we can't extract a good URL, abort without breaking the links + if (!window.isURL(href)) { + return; + } + + let href_url = new URL(href); + href_url.searchParams.delete('fbclid'); + href = href_url.toString(); + + cleanAttrs(a); + a.href = href; + a.rel = "noreferrer"; + a.target = "_blank"; + a.addEventListener("click", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mousedown", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mouseup", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mouseover", function (e) { e.stopImmediatePropagation(); }, true); +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkEnabled" +}, function (enabled) { + if (!enabled) { + return; + } + + // unwrap wrapped links in the original page + findInAllFrames(fb_wrapped_link).forEach((link) => { + cleanLink(link); + }); + + // Execute redirect unwrapping each time new content is added to the page + observeMutations(fb_wrapped_link, cleanLink); +}); diff --git a/src/js/firstparties/google-search.js b/src/js/firstparties/google-search.js new file mode 100644 index 0000000..d5eea9a --- /dev/null +++ b/src/js/firstparties/google-search.js @@ -0,0 +1,39 @@ +/* globals findInAllFrames:false */ +// In Firefox, outbound google links have the `rwt(...)` mousedown trigger. +// In Chrome, they just have a `ping` attribute. +let trap_link = "a[onmousedown^='return rwt(this,'], a[ping]"; + +// Remove excessive attributes and event listeners from link a +function cleanLink(a) { + // remove all attributes except for href, + // target (to support "Open each selected result in a new browser window"), + // class, style and ARIA attributes + for (let i = a.attributes.length - 1; i >= 0; --i) { + const attr = a.attributes[i]; + if (attr.name !== 'href' && attr.name !== 'target' && + attr.name !== 'class' && attr.name !== 'style' && + !attr.name.startsWith('aria-')) { + a.removeAttribute(attr.name); + } + } + a.rel = "noreferrer noopener"; + + // block event listeners on the link + a.addEventListener("click", function (e) { e.stopImmediatePropagation(); }, true); + a.addEventListener("mousedown", function (e) { e.stopImmediatePropagation(); }, true); +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkEnabled" +}, function (enabled) { + if (!enabled) { + return; + } + + // since the page is rendered all at once, no need to set up a + // mutationObserver or setInterval + findInAllFrames(trap_link).forEach((link) => { + cleanLink(link); + }); +}); diff --git a/src/js/firstparties/google-static.js b/src/js/firstparties/google-static.js new file mode 100644 index 0000000..cf73004 --- /dev/null +++ b/src/js/firstparties/google-static.js @@ -0,0 +1,41 @@ +let g_wrapped_link = "a[href^='https://www.google.com/url?']"; + +// Unwrap a Hangouts tracking link +function unwrapLink(a) { + let href = new URL(a.href).searchParams.get('q'); + if (!window.isURL(href)) { + return; + } + + // remove all attributes except for target, class, style and aria-* + // attributes. This should prevent the script from breaking styles and + // features for people with disabilities. + for (let i = a.attributes.length - 1; i >= 0; --i) { + const attr = a.attributes[i]; + if (attr.name !== 'target' && attr.name !== 'class' && + attr.name !== 'style' && !attr.name.startsWith('aria-')) { + a.removeAttribute(attr.name); + } + } + + a.rel = "noreferrer"; + a.href = href; +} + +// Scan the page for all wrapped links +function unwrapAll() { + document.querySelectorAll(g_wrapped_link).forEach((a) => { + unwrapLink(a); + }); +} + +// TODO race condition; fix waiting on https://crbug.com/478183 +chrome.runtime.sendMessage({ + type: "checkEnabled" +}, function (enabled) { + if (!enabled) { + return; + } + unwrapAll(); + setInterval(unwrapAll, 2000); +}); diff --git a/src/js/firstparties/lib/utils.js b/src/js/firstparties/lib/utils.js new file mode 100644 index 0000000..3edb0c8 --- /dev/null +++ b/src/js/firstparties/lib/utils.js @@ -0,0 +1,62 @@ +window.isURL = function(url) { + // ensure the URL starts with HTTP or HTTPS; otherwise we might be vulnerable + // to XSS attacks + return (url && (url.startsWith("https://") || url.startsWith("http://"))); +}; + +/** + * Search a window and all IFrames within it for a query selector, then return a + * list of all the elements in any frame that match. + */ +window.findInAllFrames = function(query) { + let out = []; + document.querySelectorAll(query).forEach((node) => { + out.push(node); + }); + Array.from(document.getElementsByTagName('iframe')).forEach((iframe) => { + try { + iframe.contentDocument.querySelectorAll(query).forEach((node) => { + out.push(node); + }); + } catch (e) { + // pass on cross origin iframe errors + } + }); + return out; +}; + +/** + * Listen for mutations on a page. If any nodes that match `selector` are added + * to the page, execute the function `callback` on them. + * Used by first-party scripts to listen for new links being added to the page + * and strip them of tracking code immediately. + */ +window.observeMutations = function(selector, callback) { + // Check all new nodes added by a mutation for tracking links and unwrap them + function onMutation(mutation) { + if (!mutation.addedNodes.length) { + return; + } + for (let node of mutation.addedNodes) { + // Only act on element nodes, otherwise querySelectorAll won't work + if (node.nodeType != Node.ELEMENT_NODE) { + continue; + } + + // check all child nodes against the selector first + node.querySelectorAll(selector).forEach((element) => { + callback(element); + }); + + // then check the node itself + if (node.matches(selector)) { + callback(node); + } + } + } + + // Set up a mutation observer with the constructed callback + new MutationObserver(function(mutations) { + mutations.forEach(onMutation); + }).observe(document, {childList: true, subtree: true, attributes: false, characterData: false}); +}; diff --git a/src/js/heuristicblocking.js b/src/js/heuristicblocking.js new file mode 100644 index 0000000..b0d3bc9 --- /dev/null +++ b/src/js/heuristicblocking.js @@ -0,0 +1,557 @@ +/* + * 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, URI:false */ + +var constants = require("constants"); +var utils = require("utils"); + +require.scopes.heuristicblocking = (function() { + + + +/*********************** heuristicblocking scope **/ +// make heuristic obj with utils and storage properties and put the things on it +function HeuristicBlocker(pbStorage) { + this.storage = pbStorage; + + // TODO roll into tabData? -- 6/10/2019 not for now, since tabData is populated + // by the synchronous listeners in webrequests.js and tabOrigins is used by the + // async listeners here; there's no way to enforce ordering of requests among + // those two. Also, tabData is cleaned up every time a tab is closed, so + // dangling requests that don't trigger listeners until after the tab closes are + // impossible to attribute to a tab. + this.tabOrigins = {}; + this.tabUrls = {}; +} + +HeuristicBlocker.prototype = { + + /** + * Blocklists an FQDN/origin: + * + * - Blocks or cookieblocks an FQDN. + * - Blocks or cookieblocks its base domain. + * - Cookieblocks any yellowlisted subdomains that share the base domain with the FQDN. + * + * @param {String} base The base domain (etld+1) to blocklist + * @param {String} fqdn The FQDN + */ + blocklistOrigin: function (base, fqdn) { + let self = this, + ylistStorage = self.storage.getStore("cookieblock_list"); + + // cookieblock or block the base domain + if (ylistStorage.hasItem(base)) { + self.storage.setupHeuristicAction(base, constants.COOKIEBLOCK); + } else { + self.storage.setupHeuristicAction(base, constants.BLOCK); + } + + // cookieblock or block the fqdn + // + // cookieblock if a "parent" domain of the fqdn is on the yellowlist + // + // 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 + let set = false, + subdomains = utils.explodeSubdomains(fqdn, true); + for (let i = 0; i < subdomains.length; i++) { + if (ylistStorage.hasItem(subdomains[i])) { + set = true; + break; + } + } + if (set) { + self.storage.setupHeuristicAction(fqdn, constants.COOKIEBLOCK); + } else { + self.storage.setupHeuristicAction(fqdn, constants.BLOCK); + } + + // cookieblock any yellowlisted subdomains with the same base domain + // + // for example, when google.com is blocked, + // books.google.com should be cookieblocked + let base_with_dot = '.' + base; + ylistStorage.keys().forEach(domain => { + if (base != domain && domain.endsWith(base_with_dot)) { + self.storage.setupHeuristicAction(domain, constants.COOKIEBLOCK); + } + }); + + }, + + /** + * Wraps _recordPrevalence for use from webRequest listeners. + * Use updateTrackerPrevalence for non-webRequest initiated bookkeeping. + * + * @param {Object} details request/response details + */ + heuristicBlockingAccounting: function (details) { + // ignore requests that are outside a tabbed window + if (details.tabId < 0 || !badger.isLearningEnabled(details.tabId)) { + return {}; + } + + let self = this, + request_host = (new URI(details.url)).host, + request_origin = window.getBaseDomain(request_host); + + // if this is a main window request, update tab data and quit + if (details.type == "main_frame") { + self.tabOrigins[details.tabId] = request_origin; + self.tabUrls[details.tabId] = details.url; + return {}; + } + + let tab_origin = self.tabOrigins[details.tabId]; + + // ignore first-party requests + if (!tab_origin || !utils.isThirdPartyDomain(request_origin, tab_origin)) { + return {}; + } + + // short-circuit if we already observed this origin tracking on this site + let firstParties = self.storage.getStore('snitch_map').getItem(request_origin); + if (firstParties && firstParties.indexOf(tab_origin) > -1) { + return {}; + } + + // abort if we already made a decision for this FQDN + let action = self.storage.getAction(request_host); + if (action != constants.NO_TRACKING && action != constants.ALLOW) { + return {}; + } + + // check if there are tracking cookies + if (hasCookieTracking(details, request_origin)) { + self._recordPrevalence(request_host, request_origin, tab_origin); + return {}; + } + }, + + /** + * Wraps _recordPrevalence for use outside of webRequest listeners. + * + * @param {String} tracker_fqdn The fully qualified domain name of the tracker + * @param {String} tracker_origin Base domain of the third party tracker + * @param {String} page_origin Base domain of page where tracking occurred + */ + updateTrackerPrevalence: function (tracker_fqdn, tracker_origin, page_origin) { + // abort if we already made a decision for this fqdn + let action = this.storage.getAction(tracker_fqdn); + if (action != constants.NO_TRACKING && action != constants.ALLOW) { + return; + } + + this._recordPrevalence( + tracker_fqdn, + tracker_origin, + page_origin + ); + }, + + /** + * Record HTTP request prevalence. Block a tracker if seen on more + * than constants.TRACKING_THRESHOLD pages + * + * NOTE: This is a private function and should never be called directly. + * All calls should be routed through heuristicBlockingAccounting for normal usage + * and updateTrackerPrevalence for manual modifications (e.g. importing + * tracker lists). + * + * @param {String} tracker_fqdn The FQDN of the third party tracker + * @param {String} tracker_origin Base domain of the third party tracker + * @param {String} page_origin Base domain of page where tracking occurred + */ + _recordPrevalence: function (tracker_fqdn, tracker_origin, page_origin) { + var snitchMap = this.storage.getStore('snitch_map'); + var firstParties = []; + if (snitchMap.hasItem(tracker_origin)) { + firstParties = snitchMap.getItem(tracker_origin); + } + + // GDPR Consent Management Provider + // https://github.com/EFForg/privacybadger/pull/2245#issuecomment-545545717 + if (tracker_origin == "consensu.org") { + return; + } + + if (firstParties.indexOf(page_origin) != -1) { + return; // We already know about the presence of this tracker on the given domain + } + + // record that we've seen this tracker on this domain (in snitch map) + firstParties.push(page_origin); + snitchMap.setItem(tracker_origin, firstParties); + + // ALLOW indicates this is a tracker still below TRACKING_THRESHOLD + // (vs. NO_TRACKING for resources we haven't seen perform tracking yet). + // see https://github.com/EFForg/privacybadger/pull/1145#discussion_r96676710 + this.storage.setupHeuristicAction(tracker_fqdn, constants.ALLOW); + this.storage.setupHeuristicAction(tracker_origin, constants.ALLOW); + + // Blocking based on outbound cookies + var httpRequestPrevalence = firstParties.length; + + // block the origin if it has been seen on multiple first party domains + if (httpRequestPrevalence >= constants.TRACKING_THRESHOLD) { + log('blocklisting origin', tracker_fqdn); + this.blocklistOrigin(tracker_origin, tracker_fqdn); + } + } +}; + + +// This maps cookies to a rough estimate of how many bits of +// identifying info we might be letting past by allowing them. +// (map values to lower case before using) +// TODO: We need a better heuristic +var lowEntropyCookieValues = { + "":3, + "nodata":3, + "no_data":3, + "yes":3, + "no":3, + "true":3, + "false":3, + "dnt":3, + "opt-out":3, + "optout":3, + "opt_out":3, + "0":4, + "1":4, + "2":4, + "3":4, + "4":4, + "5":4, + "6":4, + "7":4, + "8":4, + "9":4, + // ISO 639-1 language codes + "aa":8, + "ab":8, + "ae":8, + "af":8, + "ak":8, + "am":8, + "an":8, + "ar":8, + "as":8, + "av":8, + "ay":8, + "az":8, + "ba":8, + "be":8, + "bg":8, + "bh":8, + "bi":8, + "bm":8, + "bn":8, + "bo":8, + "br":8, + "bs":8, + "by":8, + "ca":8, + "ce":8, + "ch":8, + "co":8, + "cr":8, + "cs":8, + "cu":8, + "cv":8, + "cy":8, + "da":8, + "de":8, + "dv":8, + "dz":8, + "ee":8, + "el":8, + "en":8, + "eo":8, + "es":8, + "et":8, + "eu":8, + "fa":8, + "ff":8, + "fi":8, + "fj":8, + "fo":8, + "fr":8, + "fy":8, + "ga":8, + "gd":8, + "gl":8, + "gn":8, + "gu":8, + "gv":8, + "ha":8, + "he":8, + "hi":8, + "ho":8, + "hr":8, + "ht":8, + "hu":8, + "hy":8, + "hz":8, + "ia":8, + "id":8, + "ie":8, + "ig":8, + "ii":8, + "ik":8, + "in":8, + "io":8, + "is":8, + "it":8, + "iu":8, + "ja":8, + "jv":8, + "ka":8, + "kg":8, + "ki":8, + "kj":8, + "kk":8, + "kl":8, + "km":8, + "kn":8, + "ko":8, + "kr":8, + "ks":8, + "ku":8, + "kv":8, + "kw":8, + "ky":8, + "la":8, + "lb":8, + "lg":8, + "li":8, + "ln":8, + "lo":8, + "lt":8, + "lu":8, + "lv":8, + "mg":8, + "mh":8, + "mi":8, + "mk":8, + "ml":8, + "mn":8, + "mr":8, + "ms":8, + "mt":8, + "my":8, + "na":8, + "nb":8, + "nd":8, + "ne":8, + "ng":8, + "nl":8, + "nn":8, + "nr":8, + "nv":8, + "ny":8, + "oc":8, + "of":8, + "oj":8, + "om":8, + "or":8, + "os":8, + "pa":8, + "pi":8, + "pl":8, + "ps":8, + "pt":8, + "qu":8, + "rm":8, + "rn":8, + "ro":8, + "ru":8, + "rw":8, + "sa":8, + "sc":8, + "sd":8, + "se":8, + "sg":8, + "si":8, + "sk":8, + "sl":8, + "sm":8, + "sn":8, + "so":8, + "sq":8, + "sr":8, + "ss":8, + "st":8, + "su":8, + "sv":8, + "sw":8, + "ta":8, + "te":8, + "tg":8, + "th":8, + "ti":8, + "tk":8, + "tl":8, + "tn":8, + "to":8, + "tr":8, + "ts":8, + "tt":8, + "tw":8, + "ty":8, + "ug":8, + "uk":8, + "ur":8, + "uz":8, + "ve":8, + "vi":8, + "vo":8, + "wa":8, + "wo":8, + "xh":8, + "yi":8, + "yo":8, + "za":8, + "zh":8, + "zu":8 +}; + +/** + * Extract cookies from onBeforeSendHeaders + * + * @param details Details for onBeforeSendHeaders + * @returns {*} an array combining all Cookies + */ +function _extractCookies(details) { + let cookies = [], + headers = []; + + if (details.requestHeaders) { + headers = details.requestHeaders; + } else if (details.responseHeaders) { + headers = details.responseHeaders; + } + + for (let i = 0; i < headers.length; i++) { + let header = headers[i]; + if (header.name.toLowerCase() == "cookie" || header.name.toLowerCase() == "set-cookie") { + cookies.push(header.value); + } + } + + return cookies; +} + +/** + * Check if page is doing cookie tracking. Doing this by estimating the entropy of the cookies + * + * @param details details onBeforeSendHeaders details + * @param {String} origin URL + * @returns {boolean} true if it has cookie tracking + */ +function hasCookieTracking(details, origin) { + let cookies = _extractCookies(details); + if (!cookies.length) { + return false; + } + + let estimatedEntropy = 0; + + // loop over every cookie + for (let i = 0; i < cookies.length; i++) { + let cookie = utils.parseCookie(cookies[i], { + noDecode: true, + skipAttributes: true, + skipNonValues: true + }); + + // loop over every name/value pair in every cookie + for (let name in cookie) { + if (!cookie.hasOwnProperty(name)) { + continue; + } + + // ignore CloudFlare + // https://support.cloudflare.com/hc/en-us/articles/200170156-Understanding-the-Cloudflare-Cookies + if (name == "__cfduid" || name == "__cf_bm") { + continue; + } + + let value = cookie[name].toLowerCase(); + + if (!(value in lowEntropyCookieValues)) { + return true; + } + + estimatedEntropy += lowEntropyCookieValues[value]; + } + } + + log("All cookies for " + origin + " deemed low entropy..."); + if (estimatedEntropy > constants.MAX_COOKIE_ENTROPY) { + log("But total estimated entropy is " + estimatedEntropy + " bits, so blocking"); + return true; + } + + return false; +} + +function startListeners() { + /** + * Adds heuristicBlockingAccounting as listened to onBeforeSendHeaders request + */ + let extraInfoSpec = ['requestHeaders']; + if (chrome.webRequest.OnBeforeSendHeadersOptions.hasOwnProperty('EXTRA_HEADERS')) { + extraInfoSpec.push('extraHeaders'); + } + chrome.webRequest.onBeforeSendHeaders.addListener(function(details) { + return badger.heuristicBlocking.heuristicBlockingAccounting(details); + }, {urls: ["<all_urls>"]}, extraInfoSpec); + + /** + * Adds onResponseStarted listener. Monitor for cookies + */ + extraInfoSpec = ['responseHeaders']; + if (chrome.webRequest.OnResponseStartedOptions.hasOwnProperty('EXTRA_HEADERS')) { + extraInfoSpec.push('extraHeaders'); + } + chrome.webRequest.onResponseStarted.addListener(function(details) { + var hasSetCookie = false; + for (var i = 0; i < details.responseHeaders.length; i++) { + if (details.responseHeaders[i].name.toLowerCase() == "set-cookie") { + hasSetCookie = true; + break; + } + } + if (hasSetCookie) { + return badger.heuristicBlocking.heuristicBlockingAccounting(details); + } + }, + {urls: ["<all_urls>"]}, extraInfoSpec); +} + +/************************************** exports */ +var exports = {}; +exports.HeuristicBlocker = HeuristicBlocker; +exports.startListeners = startListeners; +exports.hasCookieTracking = hasCookieTracking; +return exports; +/************************************** exports */ +})(); diff --git a/src/js/htmlutils.js b/src/js/htmlutils.js new file mode 100644 index 0000000..2c015d4 --- /dev/null +++ b/src/js/htmlutils.js @@ -0,0 +1,283 @@ +/* + * 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/>. + */ + +require.scopes.htmlutils = (function() { + +const i18n = chrome.i18n; +const constants = require("constants"); + +let htmlUtils = { + + // default Tooltipster config + TOOLTIPSTER_DEFAULTS: { + // allow per-instance option overriding + functionInit: function (instance, helper) { + let dataOptions = helper.origin.dataset.tooltipster; + + if (dataOptions) { + try { + dataOptions = JSON.parse(dataOptions); + } catch (e) { + console.error(e); + } + + for (let name in dataOptions) { + instance.option(name, dataOptions[name]); + } + } + }, + }, + + // Tooltipster config for domain list tooltips + DOMAIN_TOOLTIP_CONF: { + delay: 100, + side: 'bottom', + }, + + /** + * Gets localized description for given action and origin. + * + * @param {String} action The action to get description for. + * @param {String} origin The origin to get description for. + * @returns {String} Localized action description with origin. + */ + getActionDescription: (function () { + const messages = { + block: i18n.getMessage('badger_status_block', "XXX"), + cookieblock: i18n.getMessage('badger_status_cookieblock', "XXX"), + noaction: i18n.getMessage('badger_status_noaction', "XXX"), + allow: i18n.getMessage('badger_status_allow', "XXX"), + dntTooltip: i18n.getMessage('dnt_tooltip') + }; + return function (action, origin) { + if (action == constants.DNT) { + return messages.dntTooltip; + } + + const rv_action = messages[action]; + + if (!rv_action) { + return origin; + } + + return rv_action.replace("XXX", origin); + }; + }()), + + /** + * Gets HTML for origin action toggle switch (block, block cookies, allow). + * + * @param {String} origin Origin to get toggle for. + * @param {String} action Current action of given origin. + * @returns {String} HTML for toggle switch. + */ + getToggleHtml: (function () { + + function is_checked(input_action, origin_action) { + if ((origin_action == constants.NO_TRACKING) || (origin_action == constants.DNT)) { + origin_action = constants.ALLOW; + } + return (input_action === origin_action ? 'checked' : ''); + } + + let tooltips = { + block: i18n.getMessage('domain_slider_block_tooltip'), + cookieblock: i18n.getMessage('domain_slider_cookieblock_tooltip'), + allow: i18n.getMessage('domain_slider_allow_tooltip') + }; + + return function (origin, action) { + let origin_id = origin.replace(/\./g, '-'); + + return ` +<div class="switch-container ${action}"> + <div class="switch-toggle switch-3 switch-candy"> + <input id="block-${origin_id}" name="${origin}" value="${constants.BLOCK}" type="radio" ${is_checked(constants.BLOCK, action)}> + <label title="${tooltips.block}" class="tooltip" for="block-${origin_id}"></label> + <input id="cookieblock-${origin_id}" name="${origin}" value="${constants.COOKIEBLOCK}" type="radio" ${is_checked(constants.COOKIEBLOCK, action)}> + <label title="${tooltips.cookieblock}" class="tooltip" for="cookieblock-${origin_id}"></label> + <input id="allow-${origin_id}" name="${origin}" value="${constants.ALLOW}" type="radio" ${is_checked(constants.ALLOW, action)}> + <label title="${tooltips.allow}" class="tooltip" for="allow-${origin_id}"></label> + <a></a> + </div> +</div> + `.trim(); + }; + + }()), + + /** + * Get HTML for tracker container. + * + * @returns {String} HTML for empty tracker container. + */ + getTrackerContainerHtml: function() { + return ` +<div class="keyContainer"> + <div class="key"> + <img src="/icons/UI-icons-red.svg" class="tooltip" title="${i18n.getMessage("tooltip_block")}"><img src="/icons/UI-icons-yellow.svg" class="tooltip" title="${i18n.getMessage("tooltip_cookieblock")}"><img src="/icons/UI-icons-green.svg" class="tooltip" title="${i18n.getMessage("tooltip_allow")}"> + </div> +</div> +<div class="spacer"></div> +<div id="blockedResourcesInner" class="clickerContainer"></div> + `.trim(); + }, + + /** + * Generates HTML for given origin. + * + * @param {String} origin Origin to get HTML for. + * @param {String} action Action for given origin. + * @param {Boolean} show_breakage_warning + * @returns {String} Origin HTML. + */ + getOriginHtml: (function () { + + const breakage_warning_tooltip = i18n.getMessage('breakage_warning_tooltip'), + undo_arrow_tooltip = i18n.getMessage('feed_the_badger_title'), + dnt_icon_url = chrome.runtime.getURL('/icons/dnt-16.png'); + + return function (origin, action, show_breakage_warning) { + action = _.escape(action); + origin = _.escape(origin); + + // Get classes for main div. + let classes = ['clicker']; + if (action.startsWith('user')) { + classes.push('userset'); + action = action.slice(5); + } + // show warning when manually blocking a domain + // that would have been cookieblocked otherwise + if (show_breakage_warning) { + classes.push('show-breakage-warning'); + } + + // If origin has been whitelisted set text for DNT. + let dnt_html = ''; + if (action == constants.DNT) { + dnt_html = ` +<div id="dnt-compliant"> + <a target=_blank href="https://privacybadger.org/#-I-am-an-online-advertising-tracking-company.--How-do-I-stop-Privacy-Badger-from-blocking-me"><img src="${dnt_icon_url}"></a> +</div> + `.trim(); + } + + // Construct HTML for origin. + let origin_tooltip = htmlUtils.getActionDescription(action, origin); + return ` +<div class="${classes.join(' ')}" data-origin="${origin}"> + <div class="origin"> + <span class="ui-icon ui-icon-alert tooltip breakage-warning" title="${breakage_warning_tooltip}"></span> + <span class="origin-inner tooltip" title="${origin_tooltip}">${dnt_html}${origin}</span> + </div> + <a href="" class="removeOrigin">✖</a> + ${htmlUtils.getToggleHtml(origin, action)} + <a href="" class="honeybadgerPowered tooltip" title="${undo_arrow_tooltip}"></a> +</div> + `.trim(); + }; + + }()), + + /** + * Toggles undo arrows and breakage warnings in domain slider rows. + * TODO rename/refactor with updateOrigin() + * + * @param {jQuery} $clicker + * @param {Boolean} userset whether to show a revert control arrow + * @param {Boolean} show_breakage_warning whether to show a breakage warning + */ + toggleBlockedStatus: function ($clicker, userset, show_breakage_warning) { + $clicker.removeClass([ + "userset", + "show-breakage-warning", + ].join(" ")); + + // toggles revert control arrow via CSS + if (userset) { + $clicker.addClass("userset"); + } + + // show warning when manually blocking a domain + // that would have been cookieblocked otherwise + if (show_breakage_warning) { + $clicker.addClass("show-breakage-warning"); + } + }, + + /** + * Compare two domains, reversing them to start comparing the least + * significant parts (TLD) first. + * + * @param {Array} domains The domains to sort. + * @returns {Array} Sorted domains. + */ + sortDomains: (domains) => { + // optimization: cache makeSortable output by walking the array once + // to extract the actual values used for sorting into a temporary array + return domains.map((domain, i) => { + return { + index: i, + value: htmlUtils.makeSortable(domain) + }; + // sort the temporary array + }).sort((a, b) => { + if (a.value > b.value) { + return 1; + } + if (a.value < b.value) { + return -1; + } + return 0; + // walk the temporary array to achieve the right order + }).map(item => domains[item.index]); + }, + + /** + * Reverse order of domain items to have the least exact (TLD) first. + * + * @param {String} domain The domain to shuffle + * @returns {String} The 'reversed' domain + */ + makeSortable: (domain) => { + let base = window.getBaseDomain(domain), + base_minus_tld = base, + dot_index = base.indexOf('.'), + rest_of_it_reversed = ''; + + if (domain.length > base.length) { + rest_of_it_reversed = domain + .slice(0, domain.length - base.length - 1) + .split('.').reverse().join('.'); + } + + if (dot_index > -1 && !window.isIPv4(domain) && !window.isIPv6(domain)) { + base_minus_tld = base.slice(0, dot_index); + } + + return (base_minus_tld + '.' + rest_of_it_reversed); + }, + +}; + +let exports = { + htmlUtils, +}; +return exports; + +})(); diff --git a/src/js/incognito.js b/src/js/incognito.js new file mode 100644 index 0000000..56d2d93 --- /dev/null +++ b/src/js/incognito.js @@ -0,0 +1,49 @@ +/* globals badger:false */ + +require.scopes.incognito = (function() { +var tabs = {}; + +// Get all existing tabs +chrome.tabs.query({}, function(results) { + results.forEach(function(tab) { + tabs[tab.id] = tab.incognito; + }); +}); + +// Create tab event listeners +function onUpdatedListener(tabId, changeInfo, tab) { + tabs[tab.id] = tab.incognito; +} + +function onRemovedListener(tabId) { + delete tabs[tabId]; +} + +// Subscribe to tab events +function startListeners() { + chrome.tabs.onUpdated.addListener(onUpdatedListener); + chrome.tabs.onRemoved.addListener(onRemovedListener); +} + +function learningEnabled(tab_id) { + if (badger.getSettings().getItem("learnInIncognito")) { + // treat all pages as if they're not incognito + return true; + } + // if we don't have incognito data for whatever reason, + // default to disabled + if (!tabs.hasOwnProperty(tab_id)) { + return false; + } + // else, do not learn in incognito tabs + return !tabs[tab_id]; +} + +/************************************** exports */ +let exports = { + learningEnabled, + startListeners, +}; +return exports; +/************************************** exports */ +})(); diff --git a/src/js/migrations.js b/src/js/migrations.js new file mode 100644 index 0000000..cea4d37 --- /dev/null +++ b/src/js/migrations.js @@ -0,0 +1,356 @@ +/* + * 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/>. + */ + +require.scopes.migrations = (function () { + +let utils = require("utils"); +let constants = require("constants"); + +let noop = function () {}; + +let exports = {}; + +exports.Migrations= { + changePrivacySettings: noop, + migrateAbpToStorage: noop, + + migrateBlockedSubdomainsToCookieblock: function(badger) { + setTimeout(function() { + console.log('MIGRATING BLOCKED SUBDOMAINS THAT ARE ON COOKIE BLOCK LIST'); + let ylist = badger.storage.getStore('cookieblock_list'); + badger.storage.getAllDomainsByPresumedAction(constants.BLOCK).forEach(fqdn => { + utils.explodeSubdomains(fqdn, true).forEach(domain => { + if (ylist.hasItem(domain)) { + console.log('moving', fqdn, 'from block to cookie block'); + badger.storage.setupHeuristicAction(fqdn, constants.COOKIEBLOCK); + } + }); + }); + }, 1000 * 30); + }, + + migrateLegacyFirefoxData: noop, + + migrateDntRecheckTimes: function(badger) { + var action_map = badger.storage.getStore('action_map'); + for (var domain in action_map.getItemClones()) { + if (badger.storage.getNextUpdateForDomain(domain) === 0) { + // Recheck at a random time in the next week + var recheckTime = _.random(Date.now(), utils.nDaysFromNow(7)); + badger.storage.touchDNTRecheckTime(domain, recheckTime); + } + } + + }, + + // Fixes https://github.com/EFForg/privacybadger/issues/1181 + migrateDntRecheckTimes2: function(badger) { + console.log('fixing DNT check times'); + var action_map = badger.storage.getStore('action_map'); + for (var domain in action_map.getItemClones()) { + // Recheck at a random time in the next week + var recheckTime = _.random(utils.oneDayFromNow(), utils.nDaysFromNow(7)); + badger.storage.touchDNTRecheckTime(domain, recheckTime); + } + }, + + forgetMistakenlyBlockedDomains: function (badger) { + console.log("Running migration to forget mistakenly flagged domains ..."); + + const MISTAKES = new Set([ + '2mdn.net', + 'akamaized.net', + 'bootcss.com', + 'cloudinary.com', + 'edgesuite.net', + 'ehowcdn.com', + 'ewscloud.com', + 'fncstatic.com', + 'fontawesome.com', + 'hgmsites.net', + 'hsforms.net', + 'hubspot.com', + 'jsdelivr.net', + 'jwplayer.com', + 'jwpsrv.com', + 'kinja-img.com', + 'kxcdn.com', + 'ldwgroup.com', + 'metapix.net', + 'optnmstr.com', + 'parastorage.com', + 'polyfill.io', + 'qbox.me', + 'rfdcontent.com', + 'scene7.com', + 'sinaimg.cn', + 'slidesharecdn.com', + 'staticworld.net', + 'taleo.net', + 'techhive.com', + 'unpkg.com', + 'uvcdn.com', + 'washingtonpost.com', + 'wixstatic.com', + 'ykimg.com', + ]); + + const actionMap = badger.storage.getStore("action_map"), + actions = actionMap.getItemClones(), + snitchMap = badger.storage.getStore("snitch_map"); + + for (let domain in actions) { + const base = window.getBaseDomain(domain); + + if (!MISTAKES.has(base)) { + continue; + } + + // remove only if + // user did not set an override + // and domain was seen tracking + const map = actions[domain]; + if (map.userAction != "" || ( + map.heuristicAction != constants.ALLOW && + map.heuristicAction != constants.BLOCK && + map.heuristicAction != constants.COOKIEBLOCK + )) { + continue; + } + + console.log("Removing %s ...", domain); + actionMap.deleteItem(domain); + snitchMap.deleteItem(base); + } + }, + + unblockIncorrectlyBlockedDomains: function (badger) { + console.log("Running migration to unblock likely incorrectly blocked domains ..."); + + let action_map = badger.storage.getStore("action_map"), + snitch_map = badger.storage.getStore("snitch_map"); + + // for every blocked domain + for (let domain in action_map.getItemClones()) { + if (action_map.getItem(domain).heuristicAction != constants.BLOCK) { + continue; + } + + let base_domain = window.getBaseDomain(domain); + + // let's check snitch map + // to see what state the blocked domain should be in instead + let sites = snitch_map.getItem(base_domain); + + // default to "no tracking" + // using "" and not constants.NO_TRACKING to match current behavior + let action = ""; + + if (sites && sites.length) { + if (sites.length >= constants.TRACKING_THRESHOLD) { + // tracking domain over threshold, set it to cookieblock or block + badger.heuristicBlocking.blocklistOrigin(base_domain, domain); + continue; + + } else { + // tracking domain below threshold + action = constants.ALLOW; + } + } + + badger.storage.setupHeuristicAction(domain, action); + } + }, + + forgetBlockedDNTDomains: function(badger) { + console.log('Running migration to forget mistakenly blocked DNT domains'); + + let action_map = badger.storage.getStore("action_map"), + snitch_map = badger.storage.getStore("snitch_map"), + domainsToFix = new Set(['eff.org', 'medium.com']); + + for (let domain in action_map.getItemClones()) { + let base = window.getBaseDomain(domain); + if (domainsToFix.has(base)) { + action_map.deleteItem(domain); + snitch_map.deleteItem(base); + } + } + }, + + reapplyYellowlist: function (badger) { + console.log("(Re)applying yellowlist ..."); + + let blocked = badger.storage.getAllDomainsByPresumedAction( + constants.BLOCK); + + // reblock all blocked domains to trigger yellowlist logic + for (let i = 0; i < blocked.length; i++) { + let domain = blocked[i]; + badger.heuristicBlocking.blocklistOrigin( + window.getBaseDomain(domain), domain); + } + }, + + forgetNontrackingDomains: function (badger) { + console.log("Forgetting non-tracking domains ..."); + + const actionMap = badger.storage.getStore("action_map"), + actions = actionMap.getItemClones(); + + for (let domain in actions) { + const map = actions[domain]; + if (map.userAction == "" && map.heuristicAction == "") { + actionMap.deleteItem(domain); + } + } + }, + + resetWebRTCIPHandlingPolicy: noop, + + enableShowNonTrackingDomains: function (badger) { + console.log("Enabling showNonTrackingDomains for some users"); + + let actionMap = badger.storage.getStore("action_map"), + actions = actionMap.getItemClones(); + + // if we have any customized sliders + if (Object.keys(actions).some(domain => actions[domain].userAction != "")) { + // keep showing non-tracking domains in the popup + badger.getSettings().setItem("showNonTrackingDomains", true); + } + }, + + forgetFirstPartySnitches: function (badger) { + console.log("Removing first parties from snitch map..."); + let snitchMap = badger.storage.getStore("snitch_map"), + actionMap = badger.storage.getStore("action_map"), + snitchClones = snitchMap.getItemClones(), + actionClones = actionMap.getItemClones(), + correctedSites = {}; + + for (let domain in snitchClones) { + // creates new array of domains checking against the isThirdParty utility + let newSnitches = snitchClones[domain].filter( + item => utils.isThirdPartyDomain(item, domain)); + + if (newSnitches.length) { + correctedSites[domain] = newSnitches; + } + } + + // clear existing maps and then use mergeUserData to rebuild them + actionMap.updateObject({}); + snitchMap.updateObject({}); + + const data = { + snitch_map: correctedSites, + action_map: actionClones + }; + + // pass in boolean 2nd parameter to flag that it's run in a migration, preventing infinite loop + badger.mergeUserData(data, true); + }, + + forgetCloudflare: function (badger) { + let config = { + name: '__cfduid' + }; + if (badger.firstPartyDomainPotentiallyRequired) { + config.firstPartyDomain = null; + } + + chrome.cookies.getAll(config, function (cookies) { + console.log("Forgetting Cloudflare domains ..."); + + let actionMap = badger.storage.getStore("action_map"), + actionClones = actionMap.getItemClones(), + snitchMap = badger.storage.getStore("snitch_map"), + snitchClones = snitchMap.getItemClones(), + correctedSites = {}, + // assume the tracking domains seen on these sites are all Cloudflare + cfduidFirstParties = new Set(); + + cookies.forEach(function (cookie) { + // get the base domain (also removes the leading dot) + cfduidFirstParties.add(window.getBaseDomain(cookie.domain)); + }); + + for (let domain in snitchClones) { + let newSnitches = snitchClones[domain].filter( + item => !cfduidFirstParties.has(item)); + + if (newSnitches.length) { + correctedSites[domain] = newSnitches; + } + } + + // clear existing maps and then use mergeUserData to rebuild them + actionMap.updateObject({}); + snitchMap.updateObject({}); + + const data = { + snitch_map: correctedSites, + action_map: actionClones + }; + + // pass in boolean 2nd parameter to flag that it's run in a migration, preventing infinite loop + badger.mergeUserData(data, true); + }); + }, + + // https://github.com/EFForg/privacybadger/pull/2245#issuecomment-545545717 + forgetConsensu: (badger) => { + console.log("Forgetting consensu.org domains (GDPR consent provider) ..."); + badger.storage.forget("consensu.org"); + }, + + resetWebRTCIPHandlingPolicy2: function (badger) { + if (!badger.webRTCAvailable) { + return; + } + + const cpn = chrome.privacy.network; + + cpn.webRTCIPHandlingPolicy.get({}, function (result) { + if (!result.levelOfControl.endsWith('_by_this_extension')) { + return; + } + + // migrate default (disabled) setting for old Badger versions + // from Mode 3 to Mode 1 + if (result.value == 'default_public_interface_only') { + console.log("Resetting webRTCIPHandlingPolicy ..."); + cpn.webRTCIPHandlingPolicy.clear({}); + + // migrate enabled setting for more recent Badger versions + // from Mode 4 to Mode 3 + } else if (result.value == 'disable_non_proxied_udp') { + console.log("Updating WebRTC IP leak protection setting ..."); + cpn.webRTCIPHandlingPolicy.set({ + value: 'default_public_interface_only' + }); + } + }); + } + +}; + + + +return exports; +})(); //require scopes diff --git a/src/js/multiDomainFirstParties.js b/src/js/multiDomainFirstParties.js new file mode 100644 index 0000000..271d9b8 --- /dev/null +++ b/src/js/multiDomainFirstParties.js @@ -0,0 +1,4133 @@ +require.scopes.multiDomainFP = (function () { + +/** + * 2d array of related domains (etld+1), all domains owned by the same entity go into + * an array, this is later transformed for efficient lookups. + */ +let multiDomainFirstPartiesArray = [ + ["1800contacts.com", "800contacts.com"], + ["37signals.com", "basecamp.com", "basecamphq.com", "highrisehq.com"], + ["9gag.com", "9cache.com"], + ["accountonline.com", "citi.com", "citibank.com", "citicards.com", "citibankonline.com"], + [ + "adidas-group.com", + + "adidas.ae", + "adidas.at", + "adidas.be", + "adidas.ca", + "adidas.ch", + "adidas.cl", + "adidas.cn", + "adidas.co", + "adidas.co.id", + "adidas.co.in", + "adidas.co.kr", + "adidas.com", + "adidas.com.ar", + "adidas.com.au", + "adidas.com.br", + "adidas.com.co", + "adidas.com.hk", + "adidas.com.my", + "adidas.com.om", + "adidas.com.pe", + "adidas.com.ph", + "adidas.com.qa", + "adidas.com.sa", + "adidas.com.sg", + "adidas.com.tr", + "adidas.com.tw", + "adidas.com.vn", + "adidas.co.nz", + "adidas.co.th", + "adidas.co.uk", + "adidas.co.za", + "adidas.cz", + "adidas.de", + "adidas.dk", + "adidas.es", + "adidas.fi", + "adidas.fr", + "adidas.gr", + "adidas.hu", + "adidas.ie", + "adidas.it", + "adidas.jp", + "adidas.mx", + "adidas.nl", + "adidas.no", + "adidas.pe", + "adidas.pl", + "adidas.pt", + "adidas.ru", + "adidas.se", + "adidas.sk", + "adidas.us", + ], + [ + "adobe.com", + "adobeexchange.com", + "adobe.io", + "adobelogin.com", + "behance.net", + "mixamo.com", + "myportfolio.com", + "typekit.com", + ], + [ + "airbnb.com", + + "airbnb.ae", + "airbnb.al", + "airbnb.am", + "airbnb.at", + "airbnb.az", + "airbnb.ba", + "airbnb.be", + "airbnb.ca", + "airbnb.cat", + "airbnb.ch", + "airbnb.cl", + "airbnb.co.cr", + "airbnb.co.id", + "airbnb.co.il", + "airbnb.co.in", + "airbnb.co.kr", + "airbnb.com.ar", + "airbnb.com.au", + "airbnb.com.bo", + "airbnb.com.br", + "airbnb.com.bz", + "airbnb.com.co", + "airbnb.com.ec", + "airbnb.com.ee", + "airbnb.com.gt", + "airbnb.com.hk", + "airbnb.com.hn", + "airbnb.com.hr", + "airbnb.com.kh", + "airbnb.com.mt", + "airbnb.com.my", + "airbnb.com.ni", + "airbnb.com.pa", + "airbnb.com.pe", + "airbnb.com.ph", + "airbnb.com.py", + "airbnb.com.ro", + "airbnb.com.sg", + "airbnb.com.sv", + "airbnb.com.tr", + "airbnb.com.tw", + "airbnb.com.ua", + "airbnb.com.vn", + "airbnb.co.nz", + "airbnb.co.uk", + "airbnb.co.ve", + "airbnb.co.za", + "airbnb.cz", + "airbnb.de", + "airbnb.dk", + "airbnb.es", + "airbnb.fi", + "airbnb.fr", + "airbnb.gr", + "airbnb.gy", + "airbnb.hu", + "airbnb.ie", + "airbnb.is", + "airbnb.it", + "airbnb.jp", + "airbnb.la", + "airbnb.lt", + "airbnb.lu", + "airbnb.lv", + "airbnb.me", + "airbnb.mx", + "airbnb.nl", + "airbnb.no", + "airbnb.pl", + "airbnb.pt", + "airbnb.rs", + "airbnb.ru", + "airbnb.se", + "airbnb.si", + + "muscache.com", + ], + [ + "airfranceklm.com", + + "airfrance.cg", + "airfrance.ci", + "airfrance.com.cn", + "airfrance.com.do", + "airfrance.com.gh", + "airfrance.dz", + "airfrance.id", + "airfrance.in", + "airfrance.my", + "airfrance.ng", + "airfrance.pa", + "airfrance.tn", + "airfrance.vn", + + "klm.ae", + "klm.at", + "klm.aw", + "klm.be", + "klm.bg", + "klm.by", + "klm.bz", + "klm.ca", + "klm.ch", + "klm.cl", + "klm.co.ao", + "klm.co.cr", + "klm.co.id", + "klm.co.il", + "klm.co.in", + "klm.co.jp", + "klm.co.ke", + "klm.co.kr", + "klm.com", + "klm.com.ar", + "klm.com.au", + "klm.com.bh", + "klm.com.br", + "klm.com.co", + "klm.com.cy", + "klm.com.ec", + "klm.com.eg", + "klm.com.gh", + "klm.com.hk", + "klm.com.mt", + "klm.com.mx", + "klm.com.my", + "klm.com.na", + "klm.com.ng", + "klm.com.pa", + "klm.com.pe", + "klm.com.ph", + "klm.com.py", + "klm.com.qa", + "klm.com.tr", + "klm.com.tw", + "klm.com.uy", + "klm.co.nz", + "klm.co.th", + "klm.co.tz", + "klm.co.ug", + "klm.co.uk", + "klm.co.za", + "klm.co.zm", + "klm.cw", + "klm.cz", + "klm.de", + "klm.dk", + "klm.do", + "klm.es", + "klm.fi", + "klm.fr", + "klm.ge", + "klm.gr", + "klm.hr", + "klm.hu", + "klm.ie", + "klm.it", + "klm.kz", + "klm.lk", + "klm.lt", + "klm.lu", + "klm.lv", + "klm.mu", + "klm.mw", + "klm.nc", + "klm.nl", + "klm.no", + "klm.pl", + "klm.pt", + "klm.ro", + "klm.ru", + "klm.se", + "klm.sg", + "klm.sk", + "klm.sr", + "klm.sx", + "klm.ua", + "klm.us", + + "static-af.com", + "static-kl.com", + ], + [ + "alibaba.com", + + "1688.com", + "95095.com", + "9game.cn", + "aliapp.org", + "alibabacloud.co.in", + "alibabacloud.com", + "alibabacloud.com.au", + "alibabacloud.com.hk", + "alibabacloud.com.my", + "alibabacloud.com.sg", + "alibabacloud.com.tw", + "alibabacorp.com", + "alibabagroup.com", + "alibaba-inc.com", + "alicdn.com", + "alicdn.net", + "alicloud.com", + "aliexpress.com", + "aliexpress.ru", + "alifanyi.com", + "aligames.com", + "alihealth.cn", + "alihive.com", + "aliimg.com", + "alimama.com", + "alimei.com", + "aliplus.com", + "alitrip.com", + "alitrip.hk", + "aliyun.com", + "aliyuncs.com", + "aliyun-iot-share.com", + "amap.com", + "cainiao.com", + "cainiao.com.cn", + "cibntv.net", + "cnzz.com", + "dayu.com", + "dingtalkapps.com", + "dingtalk.com", + "dongting.com", + "ele.me", + "elenet.me", + "etao.com", + "feizhu.cn", + "feizhu.com", + "fliggy.com", + "fliggy.hk", + "i52hz.com", + "jiaoyimao.com", + "jingguan.ai", + "jiyoujia.com", + "juhuasuan.com", + "kumiao.com", + "laifeng.com", + "liangxinyao.com", + "mappcloud.com", + "mei.com", + "mmstat.com", + "mobmore.com", + "paike.com", + "phpwind.com", + "phpwind.net", + "puata.info", + "soku.com", + "sparenode.com", + "supet.com", + "tanx.com", + "taobao.com", + "taopiaopiao.com", + "tbcdn.cn", + "tburl.in", + "teambitionapis.com", + "teambition.com", + "teambition.net", + "tianchi-global.com", + "tmail.com", + "tmall.com", + "tmall.hk", + "ttpod.com", + "tudou.com", + "uc.cn", + "ucweb.com", + "um0.cn", + "umengcloud.com", + "umeng.co", + "umeng.com", + "umindex.com", + "umsns.com", + "umtrack.com", + "wasu.tv", + "whalecloud.com", + "www.net.cn", + "xiami.com", + "ykimg.com", + "youku.com", + "youkutv.com", + "yousuode.cn", + + + "alipay.com", + + "aliloan.com", + "alipay-cloud.com", + "alipay.cn", + "alipay-eco.com", + "alipay.hk", + "alipayobjects.com", + "alipayplus.com", + "ant-biz.com", + "ant-financial.com", + "antfin.com", + "antfin-inc.com", + "antfortune.com", + "antgroup.com", + "ant-open.com", + "antsdaq.com", + "ebuckler.com", + "fund123.cn", + "huijucai.com", + "koubei.com", + "mayiyunbao.com", + "mybank.cn", + "sinopayment.com.cn", + "ssdata.com", + "xin.xin", + "yidun.com", + "zamcs.com", + "zhisheng.com", + "zmxy.com.cn", + + "lazada.com", + + "lazada.co.id", + "lazada.com.my", + "lazada.com.ph", + "lazada.co.th", + "lazada.sg", + "lazada.vn", + ], + ["allstate.com", "myallstate.com"], + ["altra.org", "altraonline.org"], + [ + "amazon.com", + + "amazon.ae", + "amazon.ca", + "amazon.cn", + "amazon.co.jp", + "amazon.com.au", + "amazon.com.br", + "amazon.com.mx", + "amazon.com.sg", + "amazon.com.tr", + "amazon.co.uk", + "amazon.de", + "amazon.es", + "amazon.fr", + "amazon.in", + "amazon.it", + "amazon.nl", + + "audible.com", + "audible.co.jp", + "audible.com.au", + "audible.co.uk", + "audible.de", + "audible.fr", + "audible.in", + "audible.it", + + "6pm.com", + "acx.com", + "amazoninspire.com", + "aws.training", + "brilliancepublishing.com", + "comixology.com", + "createspace.com", + "dpreview.com", + "eastdane.com", + "fabric.com", + "goodreads.com", + "imdb.com", + "pillpack.com", + "primevideo.com", + "shopbop.com", + "wholefoodsmarket.com", + "woot.com", + "zappos.com", + + "twitch.tv", + "ext-twitch.tv", + "jtvnw.net", + "ttvnw.net", + + "amazonpay.com", + "media-amazon.com", + "ssl-images-amazon.com", + ], + [ + "americanexpress.com", + + "americanexpress.ca", + "americanexpress.ch", + "americanexpress.com.au", + "americanexpress.co.uk", + "americanexpress.no", + + "membershiprewards.ca", + "membershiprewards.com.ar", + "membershiprewards.com.au", + "membershiprewards.com.sg", + "membershiprewards.co.uk", + "membershiprewards.de", + + "aetclocator.com", + "americanexpressfhr.com", + "amexnetwork.com", + "amextravel.com", + "amextravelresources.com", + "thecenturionlounge.com", + "yourcarrentalclaim.com", + + "aexp-static.com", + ], + ["ameritrade.com", "tdameritrade.com"], + [ + "ancestry.com", + + "ancestry.ca", + "ancestry.com.au", + "ancestry.co.uk", + "ancestry.de", + "ancestry.fr", + "ancestry.ie", + "ancestry.it", + "ancestry.mx", + "ancestry.nl", + "ancestry.no", + "ancestry.pl", + "ancestry.se", + + "ancestrylibrary.com", + "archives.com", + "findagrave.com", + "fold3.com", + "newspapers.com", + "progenealogists.com", + "rootsweb.com", + + "ancestrylibrary.ca", + + "mfcreative.com", + "ancestrycdn.com", + ], + ["androidcentral.com", "mobilenations.com"], + [ + "apa.at", + "apa-it.at", + "apa-defacto.at", + "ots.at", + + "orf.at", + "oe24.at", + "wienerzeitung.at", + "kleinezeitung.at", + "vn.at", + "kurier.at", + "schautv.at", + "nachrichten.at", + "derstandard.at", + "sn.at", + "volksblatt.at", + "neue.at", + + "tt.com", + "diepresse.com", + ], + ["apple.com", "icloud.com", "icloud.com.cn", "cdn-apple.com"], + ["applefcu.org", "applefcuonline.org"], + ["archive.org", "openlibrary.org"], + ["asos.com", "asosservices.com"], + [ + "atlassian.com", + + "atlassian.io", + "atlassian.net", + "bitbucket.org", + "customercase.com", + "enso.me", + "hipchat.com", + "jira.com", + "statuspage.io", + "stride.com", + "trello.com", + + "atl-paas.net", + ], + [ + "att.com", + + "att.tv", + "atttvnow.com", + "attwatchtv.com", + "directv.com", + "directvnow.com", + ], + [ + "autodesk.com", + + "autodesk.io", + "autodesk.net", + "circuits.io", + "tinkercad.com", + + "autodesk.ae", + "autodesk.be", + "autodesk.ca", + "autodesk.ch", + "autodesk.co.jp", + "autodesk.co.kr", + "autodesk.com.au", + "autodesk.com.br", + "autodesk.com.cn", + "autodesk.com.hk", + "autodesk.com.my", + "autodesk.com.sg", + "autodesk.com.tr", + "autodesk.com.tw", + "autodesk.co.nz", + "autodesk.co.uk", + "autodesk.co.za", + "autodesk.cz", + "autodesk.de", + "autodesk.dk", + "autodesk.es", + "autodesk.eu", + "autodesk.fi", + "autodesk.fr", + "autodesk.hu", + "autodesk.in", + "autodesk.it", + "autodesk.mx", + "autodesk.nl", + "autodesk.no", + "autodesk.pl", + "autodesk.pt", + "autodesk.ru", + "autodesk.se", + ], + ["avito.ru", "avito.st"], + ["avon.com", "youravon.com"], + [ + "baidu.com", + + "hao123.com", + "tieba.com", + + "baidustatic.com", + "bdimg.com", + "bdstatic.com", + ], + ["balsamiq.com", "balsamiq.cloud"], + ["bancomer.com", "bancomer.com.mx", "bbvanet.com.mx"], + ["bankofamerica.com", "bofa.com", "mbna.com", "usecfo.com"], + ["bank-yahav.co.il", "bankhapoalim.co.il"], + [ + "bauermedia.co.uk", + + "carmagazine.co.uk", + "motorcyclenews.com", + "parkers.co.uk", + + "bauersecure.com", + ], + ["bbc.co.uk", "bbc.com", "bbci.co.uk"], + ["belkin.com", "seedonk.com"], + [ + "bellmedia.ca", + + "9c9media.ca", + "9c9media.com", + "animalplanet.ca", + "bardown.com", + "bnnbloomberg.ca", + "bnn.ca", + "bravo.ca", + "canald.com", + "canalvie.com", + "cinepop.ca", + "cmdy.ca", + "cookieless.ca", + "cp24.com", + "crave.ca", + "crave.com", + "cravetv.ca", + "ctv.ca", + "ctvdigital.net", + "ctvnews.ca", + "discovery.ca", + "discoveryvelocity.ca", + "envedette.ca", + "etalk.ca", + "fraichementpresse.ca", + "investigationdiscovery.ca", + "investigationtele.com", + "lookdujour.ca", + "marilyn.ca", + "mtv.ca", + "much.ca", + "much.com", + "muchmusic.com", + "muramur.ca", + "rds.ca", + "sciencechannel.ca", + "sego-cdn.com", + "space.ca", + "superecran.com", + "superecrango.com", + "sympatico.ca", + "thecomedynetwork.ca", + "theloop.ca", + "thesocial.ca", + "tmngo.ca", + "tsn.ca", + "voyagevoyage.ca", + "vrak.tv", + "ztele.com", + ], + ["bhphotovideo.com", "bandh.com", "bhphoto.com", "bnh.com"], + ["bilibili.com", "acgvideo.com", "biliapi.net", "biliapi.com", "biligame.com", "hdslb.com"], + ["binance.com", "binance.cloud", "binance.vision", "bnbstatic.com"], + ["blizzard.com", "battle.net", "worldofwarcraft.com"], + ["bloomberg.com", "bbthat.com", "bwbx.io"], + ["boardgamearena.com", "boardgamearena.net"], + ["booking.com", "bstatic.com"], + ["box.com", "boxcdn.net"], + [ + "bustle.company", + + "bustle.com", + "elitedaily.com", + "inputmag.com", + "inverse.com", + "mic.com", + "nylon.com", + "romper.com", + "thezoereport.com", + ], + [ + "canada.ca", + + "ceaa-acee.gc.ca", + "collectionscanada.gc.ca", + "cra-arc.gc.ca", + "dfo-mpo.gc.ca", + "ec.gc.ca", + "esdc.gc.ca", + "fcac-acfc.gc.ca", + "hrdc-drhc.gc.ca", + "ic.gc.ca", + "jobbank.gc.ca", + "labour.gc.ca", + "nrcan.gc.ca", + "sac-isc.gc.ca", + "servicecanada.gc.ca", + "services.gc.ca", + "statcan.gc.ca", + "tbs-sct.gc.ca", + "tc.gc.ca", + "tpsgc-pwgsc.gc.ca", + "weather.gc.ca", + + "archives.ca", + "canlearn.ca", + "gcsurplus.ca", + "letstalktransportation.ca", + ], + [ + "canalplus.com", + + "canal.fr", + "canalplay.com", + "canalplus-bo.net", + "canal-plus.com", + "canalplus.fr", + "canalplusinternational.com", + "canal-plus.net", + "canal-plus.pro", + "canalplus.pro", + "canalpro.fr", + "clique.tv", + "cstar.fr", + "mycanal.fr", + ], + ["capitalone.com", "capitalone360.com"], + [ + "cbs.com", + + "cbsi.com", + "cbsig.net", + "cbsimg.net", + "cbsinteractive.com", + "cbsistatic.com", + "cbslocal.com", + "cbsnews.com", + "cbssports.com", + "cbsstatic.com", + "chow.com", + "chowhound.com", + "chowmagazine.com", + "chowstatic.com", + "cnet.com", + "cnetcontent.com", + "cnettv.com", + "collegesports.com", + "com.com", + "comicvine.com", + "download.com", + "etonline.com", + "fansonly.com", + "gamespot.com", + "giantbomb.com", + "insideedition.com", + "last.fm", + "metacritic.com", + "news.com", + "scout.com", + "search.com", + "sho.com", + "sportsline.com", + "techrepublic.com", + "tv.com", + "tvgcdn.net", + "tvguide.com", + "upload.com", + "zdnet.com", + ], + ["cb2.com", "crateandbarrel.com"], + [ + "ccmbenchmark.com", + + "linternaute.com", + "journaldesfemmes.fr", + "journaldunet.com", + "commentcamarche.net", + + "ccmbg.com", + ], + ["century21.com", "21online.com"], + ["chart.io", "chartio.com"], + ["chaturbate.com", "highwebmedia.com"], + [ + "cisco.com", + + "ciscolive.com", + "duo.com", + "netacad.com", + "webex.com", + + "static-cisco.com", + ], + ["cms.gov", "medicare.gov", "mymedicare.gov"], + ["codepen.io", "cdpn.io"], + ["concur.com", "concursolutions.com"], + [ + "cornell.edu", + + "birdsna.org", + "birdsoftheworld.com", + "birdsoftheworld.org", + "ebird.org", + "hbw.com", + "macaulaylibrary.org", + ], + [ + "condenast.com", + + "architecturaldigest.com", + "arstechnica.com", + "bonappetit.com", + "cntraveler.com", + "epicurious.com", + "glamour.com", + "gq.com", + "lennyletter.com", + "newyorker.com", + "pitchfork.com", + "self.com", + "teenvogue.com", + "them.us", + "vanityfair.com", + "vogue.com", + "wired.com", + + "condenastdigital.com", + ], + ["cox.com", "cox.net"], + ["cricketwireless.com", "aiowireless.com"], + ["ctrip.com", "c-ctrip.com", "trip.com"], + ["dcu.org", "dcu-online.org"], + ["dictionary.com", "thesaurus.com", "sfdict.com"], + [ + "digikey.com", + + "digikey.ae", + "digikey.am", + "digikey.at", + "digikey.ba", + "digikey.be", + "digikey.bg", + "digikey.bo", + "digikey.by", + "digikey.ca", + "digikey.ch", + "digikey.cl", + "digikey.cn", + "digikey.co.id", + "digikey.co.il", + "digikey.com.ar", + "digikey.com.au", + "digikey.com.br", + "digikey.com.cn", + "digikey.com.co", + "digikey.com.cy", + "digikey.com.eg", + "digikey.com.gt", + "digikey.com.hr", + "digikey.com.jm", + "digikey.com.lb", + "digikey.com.mk", + "digikey.com.mx", + "digikey.com.pa", + "digikey.com.tr", + "digikey.com.ua", + "digikey.com.uy", + "digikey.com.ve", + "digikey.co.nz", + "digikey.co.th", + "digikey.co.uk", + "digikey.co.za", + "digikey.cr", + "digikey.cz", + "digikey.de", + "digikey.dk", + "digikey.do", + "digikey.ec", + "digikey.ee", + "digikey.es", + "digikey.fi", + "digikey.fr", + "digikey.gr", + "digikey.hk", + "digikey.hu", + "digikey.ie", + "digikey.in", + "digikey.is", + "digikey.it", + "digikey.jp", + "digikey.kr", + "digikey.lk", + "digikey.lt", + "digikey.lu", + "digikey.lv", + "digikey.ma", + "digikey.md", + "digikey.my", + "digikey.nl", + "digikey.no", + "digikey.pe", + "digikey.ph", + "digikey.pk", + "digikey.pl", + "digikey.pr", + "digikey.pt", + "digikey.ro", + "digikey.rs", + "digikey.ru", + "digikey.se", + "digikey.sg", + "digikey.si", + "digikey.sk", + "digikey.tn", + "digikey.tw", + "digikey.vn", + ], + [ + "digitec.ch", + + "galaxus.ch", + "galaxus.de", + "galaxus.fr", + + "digitecgalaxus.ch", + ], + [ + "directferries.com", + + "directferries.at", + "directferries.be", + "directferries.ca", + "directferries.ch", + "directferries.cn", + "directferries.co.id", + "directferries.co.kr", + "directferries.com.au", + "directferries.com.tr", + "directferries.com.ua", + "directferries.co.nz", + "directferries.co.uk", + "directferries.cz", + "directferries.de", + "directferries.dk", + "directferries.es", + "directferries.fi", + "directferries.fr", + "directferries.gr", + "directferries.ie", + "directferries.it", + "directferries.jp", + "directferries.kr", + "directferries.ma", + "directferries.nl", + "directferries.no", + "directferries.nz", + "directferries.pl", + "directferries.pt", + "directferries.ro", + "directferries.ru", + "directferries.se", + "directferries.sk", + "directferries.xyz", + ], + ["discountbank.co.il", "telebank.co.il"], + ["discord.com", "discordapp.com", "discordapp.net"], + ["discover.com", "discovercard.com"], + ["disqus.com", "disquscdn.com"], + [ + "dmgmedia.co.uk", + + "dailymail.co.uk", + "inews.co.uk", + "mailonsunday.co.uk", + "metro.co.uk", + "thisismoney.co.uk", + + "dmgmediaprivacy.co.uk", + ], + [ + "dpgmediagroup.com", + + "persgroep.net", + "persgroep.cloud", + + "7sur7.be", + "ad.nl", + "bd.nl", + "beursrally.be", + "beurswijzer.com", + "bndestem.nl", + "demorgen.be", + "destentor.nl", + "dpgmedia.be", + "dpgmedia.nl", + "ed.nl", + "gelderlander.nl", + "hln.be", + "humo.be", + "parool.nl", + "persgroepinternational.be", + "persgroepinternational.com", + "persgroep.nl", + "pzc.nl", + "tijd.be", + "topics.be", + "topics.nl", + "trouw.nl", + "tubantia.nl", + "volkskrant.nl", + "vtm.be", + + "dpgmedia.net", + ], + ["dropbox.com", "dropboxstatic.com", "dropboxusercontent.com", "getdropbox.com"], + ["d.rip", "kickstarter.com"], + [ + "ea.com", + + "bioware.com", + "masseffect.com", + "origin.com", + "play4free.com", + "tiberiumalliance.com", + ], + [ + "ebay.com", + "ebayinc.com", + + "ebay.at", + "ebay.be", + "ebay.ca", + "ebay.ch", + "ebay.com.au", + "ebay.com.hk", + "ebay.com.my", + "ebay.com.sg", + "ebay.co.uk", + "ebay.de", + "ebay.es", + "ebay.fr", + "ebay.ie", + "ebay.in", + "ebay.it", + "ebay.nl", + "ebay.ph", + "ebay.pl", + "vivanuncios.com.mx", + + "ebaydesc.com", + "ebayimg.com", + "ebayrtm.com", + "ebaystatic.com", + "ebay-us.com", + ], + ["elsevier.com", "sciencedirect.com", "sciencedirectassets.com"], + [ + "enterprise.com", + + "alamo.ca", + "alamo.com", + + "autoshare.com", + "autoshare.biz", + "autoshare.ca", + "autoshare.net", + "autoshare.org", + + "cars.info", + "carsharing.ca", + "carsharingtoronto.com", + "citer.fr", + + "ehi.com", + "ehiaws.com", + + "enterprise.ca", + "enterprise.ch", + "enterprise.com.jm", + "enterprise.co.uk", + "enterprise.de", + "enterprise.dk", + "enterprise.ec", + "enterprise.es", + "enterprise.fr", + "enterprise.gr", + "enterprise.hr", + "enterprise.hu", + "enterprise.ie", + "enterprise.lv", + "enterprise.nl", + "enterprise.no", + "enterprise.pt", + "enterprise.se", + + "enterprisecarclub.co.uk", + "enterprisecarclub.ie", + + "enterprisecarshare.ca", + "enterprisecarshare.com", + "enterprisecarshare.co.uk", + + "enterpriserideshare.com", + + "enterpriserentacar.at", + "enterpriserentacar.be", + "enterpriserentacar.bg", + "enterpriserentacar.ca", + "enterpriserentacar.com.au", + "enterpriserentacar.co.nz", + "enterpriserentacar.cz", + "enterpriserentacar.is", + "enterpriserentacar.it", + "enterpriserentacar.pl", + "enterpriserentacar.se", + + "nationalcar.ca", + "nationalcar.com", + "nationalcar.co.uk", + "nationalcar.de", + "nationalcar.es", + "nationalcar.fr", + "nationalcar.ie", + "nationalcar.it", + "nationalcar.mobi", + + "onewaygo.de", + + "alamo-np.ca", + "alamo-np.com", + "alamo-np.co.uk", + "alamo-np.de", + "alamo-np.es", + "alamo-np.fr", + "alamo-np.ie", + ], + ["epicgames.com", "unrealengine.com"], + [ + "eventbrite.com", + + "eventbrite.at", + "eventbrite.be", + "eventbrite.ca", + "eventbrite.ch", + "eventbrite.cl", + "eventbrite.co", + "eventbrite.com.ar", + "eventbrite.com.au", + "eventbrite.com.br", + "eventbrite.com.mx", + "eventbrite.com.ng", + "eventbrite.com.pe", + "eventbrite.co.nz", + "eventbrite.co.uk", + "eventbrite.co.za", + "eventbrite.de", + "eventbrite.dk", + "eventbrite.es", + "eventbrite.fi", + "eventbrite.fr", + "eventbrite.hk", + "eventbrite.ie", + "eventbrite.in", + "eventbrite.it", + "eventbrite.my", + "eventbrite.nl", + "eventbrite.ph", + "eventbrite.pt", + "eventbrite.se", + "eventbrite.sg", + + "evbstatic.com", + "evbuc.com", + "eventbriteapi.com", + ], + [ + "expedia.com", + + "carrentals.com", + "cheaptickets.com", + "ebookers.com", + "hotels.com", + "hotwire.com", + "mrjet.se", + "orbitz.com", + "travelocity.com", + "wotif.com", + + "expedia-aarp.com", + "expedia-barclays.co.uk", + "expedia-cn.com", + "expedia.at", + "expedia.be", + "expedia.ca", + "expedia.ch", + "expedia.cn", + "expedia.co.id", + "expedia.co.in", + "expedia.co.jp", + "expedia.co.kr", + "expedia.co.nz", + "expedia.co.th", + "expedia.co.uk", + "expedia.com.ar", + "expedia.com.au", + "expedia.com.br", + "expedia.com.hk", + "expedia.com.my", + "expedia.com.ph", + "expedia.com.sg", + "expedia.com.tw", + "expedia.com.vn", + "expedia.de", + "expedia.dk", + "expedia.es", + "expedia.fi", + "expedia.fr", + "expedia.ie", + "expedia.it", + "expedia.mx", + "expedia.nl", + "expedia.no", + "expedia.ru", + "expedia.se", + "expediacorporate.eu", + + "expedia.net", + + "travel-assets.com", + "trvl-media.com", + + "lastminute.com.au", + "lastminute.co.nz", + "wotif.com.au", + "wotif.co.nz", + + "cdn-hotels.com", + "hotels.cn", + + "hotwirestatic.com", + + "ebookers.at", + "ebookers.be", + "ebookers.ch", + "ebookers.co.uk", + "ebookers.de", + "ebookers.fi", + "ebookers.fr", + "ebookers.ie", + "ebookers.nl", + "ebookers.no", + + "mrjet.dk", + + "vrbo.com", + + "abritel.fr", + "aluguetemporada.com.br", + "fewo-direkt.de", + "homeaway.at", + "homeaway.ca", + "homeaway.com", + "homeaway.com.au", + "homeaway.com.mx", + "homeaway.co.nz", + "homeaway.co.uk", + "homeaway.dk", + "homeaway.es", + "homeaway.fi", + "homeaway.gr", + "homeaway.it", + "homeaway.nl", + "homeaway.no", + "homeaway.pl", + "homeaway.pt", + "homeaway.se", + "homelidays.com", + "homelidays.es", + "homelidays.fr", + "homelidays.it", + "ownersdirect.co.uk", + "stayz.com.au", + "vacationrentals.com", + ], + ["express-scripts.com", "medcohealth.com"], + [ + "facebook.com", + + "messenger.com", + "workplace.com", + + "oculus.com", + "oculuscdn.com", + "oculusrift.com", + "oculusvr.com", + "powersunitedvr.com", + + "facebook.net", + "fbcdn.com", + "fbcdn.net", + "fbsbx.com", + ], + [ + "faithlife.com", + + "biblescreen.com", + "biblestudymagazine.com", + "biblia.com", + "didaktikosjournal.com", + "faithlifetv.com", + "kirkdalepress.com", + "lexhampress.com", + "logos.com", + "ministrytracker.com", + "proclaimonline.com", + "verbum.com", + + "bibliacdn.com", + "faithlifecdn.com", + "faithlifesitescdn.com", + "logoscdn.com", + ], + [ + "fandom.com", + "fandom-dev.pl", + "fandom-dev.us", + "nocookie.net", + "wikia.com", + "wikia-dev.com", + "wikia-dev.pl", + "wikia-dev.us", + "wikiafanstudio.com", + "wikia-inc.com", + "wikia.net", + "wikia.org", + "wikia-services.com", + "wikia-staging.com", + ], + [ + "fastcompany.com", + + "fastcocreate.com", + "fastcodesign.com", + "fastcoexist.com", + "fastcolabs.com", + "fast-co.net", + "fcimpactcouncil.com", + "inc.com", + "innovationuncensored.com", + "mansueto.com", + "mvdigitalmedia.com", + "mvlicensing.com", + "nativguard.com", + "retirementcomm.com", + + "fastcompany.net", + ], + ["fastmail.com", "fastmailusercontent.com"], + ["firefox.com", "firefoxusercontent.com", "mozilla.org"], + ["foxnews.com", "foxbusiness.com", "fncstatic.com"], + [ + "futureplc.com", + + "creativebloq.com", + "cyclingnews.com", + "digitalcameraworld.com", + "gamesradar.com", + "gizmodo.co.uk", + "guitarworld.com", + "kotaku.co.uk", + "laptopmag.com", + "lifehacker.co.uk", + "livescience.com", + "loudersound.com", + "musicradar.com", + "pcgamer.com", + "space.com", + "t3.com", + "techradar.com", + "tomsguide.com", + "tomshardware.com", + "toptenreviews.com", + "whathifi.com", + + "futurecdn.net", + "future-fie-assets.co.uk", + "future-fie.co.uk", + "future.net.uk", + ], + ["gamestar.de", "gamepro.de", "cgames.de"], + [ + "gap.com", + + "bananarepublic.com", + "gapfactory.com", + "gapinc.com", + "oldnavy.com", + "piperlime.com", + + "bananarepublic.ca", + "bananarepublic.co.jp", + "bananarepublic.co.uk", + "bananarepublic.eu", + "gapcanada.ca", + "gap.co.jp", + "gap.co.uk", + "gap.eu", + "gap.hk", + "oldnavy.ca", + + "assets-gap.com", + ], + [ + "gedispa.it", + + "capital.it", + "deejay.it", + "gelocal.it", + "ilsecoloxix.it", + "kataweb.it", + "lastampa.it", + "lescienze.it", + "limesonline.com", + "m2o.it", + "mymovies.it", + "repubblica.it", + + "gedidigital.it", + "repstatic.it", + ], + [ + "gettyimages.com", + + "gettyimages.ca", + "gettyimages.com.au", + "gettyimages.co.uk", + "gettyimages.dk", + "gettyimages.fi", + "gettyimages.nl", + + "istockphoto.com", + + "thinkstockphotos.com", + "thinkstockphotos.ca", + ], + ["gitlab.com", "gitlab-static.net"], + [ + "gizmodo.com", + + "avclub.com", + "deadspin.com", + "jalopnik.com", + "jezebel.com", + "kinja.com", + "kinja-img.com", + "kinja-static.com", + "kotaku.com", + "lifehacker.com", + "technoratimedia.com", + "theinventory.com", + "theonion.com", + "theroot.com", + "thetakeout.com", + ], + [ + "glassdoor.com", + + "glassdoor.be", + "glassdoor.ca", + "glassdoor.co.in", + "glassdoor.com.au", + "glassdoor.co.uk", + "glassdoor.de", + "glassdoor.fr", + "glassdoor.ie", + "glassdoor.nl", + ], + ["gogoair.com", "gogoinflight.com"], + [ + "google.com", + "youtube.com", + "gmail.com", + "blogger.com", + "blog.google", + "googleblog.com", + "chromium.org", + + "ggpht.com", + "googleusercontent.com", + "googlevideo.com", + "gstatic.com", + "youtube-nocookie.com", + "ytimg.com", + + "google.ad", + "google.ae", + "google.al", + "google.am", + "google.as", + "google.at", + "google.az", + "google.ba", + "google.be", + "google.bf", + "google.bg", + "google.bi", + "google.bj", + "google.bs", + "google.bt", + "google.by", + "google.ca", + "google.cat", + "google.cd", + "google.cf", + "google.cg", + "google.ch", + "google.ci", + "google.cl", + "google.cm", + "google.cn", + "google.com.af", + "google.com.ag", + "google.com.ai", + "google.com.ar", + "google.com.au", + "google.com.bd", + "google.com.bh", + "google.com.bn", + "google.com.bo", + "google.com.br", + "google.com.bz", + "google.com.co", + "google.com.cu", + "google.com.cy", + "google.com.do", + "google.com.ec", + "google.com.eg", + "google.com.et", + "google.com.fj", + "google.com.gh", + "google.com.gi", + "google.com.gt", + "google.com.hk", + "google.com.jm", + "google.com.kh", + "google.com.kw", + "google.com.lb", + "google.com.ly", + "google.com.mm", + "google.com.mt", + "google.com.mx", + "google.com.my", + "google.com.na", + "google.com.ng", + "google.com.ni", + "google.com.np", + "google.com.om", + "google.com.pa", + "google.com.pe", + "google.com.pg", + "google.com.ph", + "google.com.pk", + "google.com.pr", + "google.com.py", + "google.com.qa", + "google.com.sa", + "google.com.sb", + "google.com.sg", + "google.com.sl", + "google.com.sv", + "google.com.tj", + "google.com.tr", + "google.com.tw", + "google.com.ua", + "google.com.uy", + "google.com.vc", + "google.com.vn", + "google.co.ao", + "google.co.bw", + "google.co.ck", + "google.co.cr", + "google.co.id", + "google.co.il", + "google.co.in", + "google.co.jp", + "google.co.ke", + "google.co.kr", + "google.co.ls", + "google.co.ma", + "google.co.mz", + "google.co.nz", + "google.co.th", + "google.co.tz", + "google.co.ug", + "google.co.uk", + "google.co.uz", + "google.co.ve", + "google.co.vi", + "google.co.za", + "google.co.zm", + "google.co.zw", + "google.cv", + "google.cz", + "google.de", + "google.dj", + "google.dk", + "google.dm", + "google.dz", + "google.ee", + "google.es", + "google.fi", + "google.fm", + "google.fr", + "google.ga", + "google.ge", + "google.gg", + "google.gl", + "google.gm", + "google.gr", + "google.gy", + "google.hn", + "google.hr", + "google.ht", + "google.hu", + "google.ie", + "google.im", + "google.iq", + "google.is", + "google.it", + "google.je", + "google.jo", + "google.kg", + "google.ki", + "google.kz", + "google.la", + "google.li", + "google.lk", + "google.lt", + "google.lu", + "google.lv", + "google.md", + "google.me", + "google.mg", + "google.mk", + "google.ml", + "google.mn", + "google.ms", + "google.mu", + "google.mv", + "google.mw", + "google.ne", + "google.nl", + "google.no", + "google.nr", + "google.nu", + "google.pl", + "google.pn", + "google.ps", + "google.pt", + "google.ro", + "google.rs", + "google.ru", + "google.rw", + "google.sc", + "google.se", + "google.sh", + "google.si", + "google.sk", + "google.sm", + "google.sn", + "google.so", + "google.sr", + "google.st", + "google.td", + "google.tg", + "google.tl", + "google.tm", + "google.tn", + "google.to", + "google.tt", + "google.vg", + "google.vu", + "google.ws", + + "fonts.googleapis.com", + "storage.googleapis.com", + "www.googleapis.com", + + "nest.com", + "codingcompetitions.withgoogle.com", + "nestpowerproject.withgoogle.com", + ], + ["www.gov.uk", "cabinet-office.gov.uk", "publishing.service.gov.uk"], + [ + "gray.tv", + + "1011northplatte.com", + "1011now.com", + "13abc.com", + "26nbc.com", + "abc12.com", + "blackhillsfox.com", + "cbs7.com", + "graydc.com", + "kalb.com", + "kbtx.com", + "kcrg.com", + "kcwy13.com", + "kfyrtv.com", + "kgns.tv", + "kgwn.tv", + "kkco11news.com", + "kktv.com", + "kmot.com", + "kmvt.com", + "knoe.com", + "knopnews2.com", + "kolotv.com", + "kotatv.com", + "kqcd.com", + "ksfy.com", + "ksnblocal4.com", + "kspr.com", + "ktuu.com", + "kumv.com", + "kwch.com", + "kwqc.com", + "kwtx.com", + "kxii.com", + "ky3.com", + "nbc15.com", + "newsplex.com", + "thenewscenter.tv", + "uppermichigansource.com", + "valleynewslive.com", + "wabi.tv", + "wagmtv.com", + "wbay.com", + "wbko.com", + "wcax.com", + "wcjb.com", + "wctv.tv", + "wdbj7.com", + "wdtv.com", + "weau.com", + "webcenter11.com", + "whsv.com", + "wibw.com", + "wifr.com", + "wilx.com", + "witn.com", + "wjhg.com", + "wkyt.com", + "wndu.com", + "wowt.com", + "wrdw.com", + "wsaw.com", + "wsaz.com", + "wswg.tv", + "wtok.com", + "wtvy.com", + "wvlt.tv", + "wymt.com", + + "graytvinc.com", + ], + ["guardian.co.uk", "guim.co.uk", "guardianapps.co.uk", "theguardian.com", "gu-web.net"], + [ + "habr.com", + "habr.ru", + "habrahabr.ru", + "freelansim.ru", + "geektimes.com", + "geektimes.ru", + "moikrug.ru", + "toster.ru", + + "habracdn.net", + "habrastorage.org", + "hsto.org", + ], + ["healthfusion.com", "healthfusionclaims.com"], + [ + "hearst.com", + + "25ans.jp", + "autoweek.com", + "bazaar.com", + "beaumontenterprise.com", + "bestproducts.com", + "bicycling.com", + "caranddriver.com", + "chron.com", + "cosmopolitan.com", + "countryliving.com", + "crfashionbook.com", + "ctnews.com", + "ctpost.com", + "dariennewsonline.com", + "delish.com", + "drozthegoodlife.com", + "elle.com", + "elledecor.com", + "esquire.com", + "expressnews.com", + "fairfieldcitizenonline.com", + "foothillstrader.com", + "fujingaho.jp", + "gametimect.com", + "gearpatrol.com", + "ghsealapplication.com", + "goodhouse.com", + "goodhousekeeping.com", + "greenwichtime.com", + "harpersbazaar.com", + "housebeautiful.com", + "houstonchronicle.com", + "lmtonline.com", + "marieclaire.com", + "menshealth.com", + "michigansthumb.com", + "middletownpress.com", + "mrt.com", + "myjournalcourier.com", + "mylo.id", + "myplainview.com", + "mysanantonio.com", + "newcanaannewsonline.com", + "newmilfordspectrum.com", + "newstimes.com", + "nhregister.com", + "oprahmag.com", + "ourmidland.com", + "popularmechanics.com", + "prevention.com", + "redbookmag.com", + "registercitizen.com", + "roadandtrack.com", + "rodalesorganiclife.com", + "runnersworld.com", + "seattlepi.com", + "seventeen.com", + "sfchronicle.com", + "sfgate.com", + "shondaland.com", + "stamfordadvocate.com", + "s-w-e-e-t.com", + "thehour.com", + "theintelligencer.com", + "thepioneerwoman.com", + "thepioneerwomancooks.com", + "thetelegraph.com", + "timesunion.com", + "todays-rewards.com", + "townandcountrymag.com", + "veranda.com", + "wearesweet.co", + "westport-news.com", + "womansday.com", + "womenshealthmag.com", + "yourconroenews.com", + + "h-cdn.co", + "hdmtech.net", + "hdmtools.com", + "hdnux.com", + "hearst3pcc.com", + "hearstapps.com", + "hearstapps.net", + "hearstdigitalstudios.com", + "hearstdigitalstudios.net", + "hearst.io", + "hearstlabs.com", + "hearstmags.com", + "hearstmobile.com", + "hearstnp.com", + ], + [ + "houzz.com", + + "houzz.at", + "houzz.be", + "houzz.ca", + "houzz.ch", + "houzz.co.jp", + "houzz.com.au", + "houzz.com.sg", + "houzz.co.nz", + "houzz.co.uk", + "houzz.de", + "houzz.dk", + "houzz.es", + "houzz.fi", + "houzz.fr", + "houzz.ie", + "houzz.in", + "houzz.it", + "houzz.jp", + "houzz.no", + "houzz.nz", + "houzz.pt", + "houzz.ru", + "houzz.se", + "houzz.sg", + "houzz.uk", + + "gardenweb.com", + "gwhouzz3.com", + "gwhouzz.com", + "houzz2.com", + "houzz2.com.au", + "houzz2.co.uk", + "houzz3.com", + "houzz3.com.au", + "houzz3.co.uk", + "hzcdn.com", + "stghouzz.com", + "thathomesite.com", + ], + [ + "huobi.com", + + "hbfile.net", + "hbg.com", + "huobiasia.vip", + "huobi.br.com", + "huobi.me", + ], + ["hvfcu.org", "hvfcuonline.org"], + [ + "idealo.de", + + "idealo.at", + "idealo.co.uk", + "idealo.es", + "idealo.fr", + "idealo.it", + "idealo.com", + ], + [ + "ign.fr", + + "cartoradio.fr", + "culture.fr", + "duministeredelaculture.fr", + "gouvernement.fr", + "ignrando.fr", + + "ants.gouv.fr", + "culture.gouv.fr", + "data.gouv.fr", + "education.gouv.fr", + "etalab.gouv.fr", + "geoportail.gouv.fr", + "geoportail-urbanisme.gouv.fr", + "impots.gouv.fr", + "premier-ministre.gouv.fr", + "service-civique.gouv.fr", + "yvelines.gouv.fr", + + "ac-grenoble.fr", + "ac-versailles.fr", + "ac-bordeaux.fr", + "ac-montpellier.fr", + "ac-lille.fr", + ], + [ + "impresa.pt", + + "blitz.pt", + "expresso.pt", + "famashow.pt", + "impresamediacriativa.pt", + "sapo.pt", + "siccaras.pt", + "sickapa.pt", + "sicmulher.pt", + "sicnoticias.pt", + "sic.pt", + "sicradical.pt", + "smack.pt", + "tribunaexpresso.pt", + "volantesic.pt", + ], + [ + "immobilienscout24.de", + "static-immobilienscout24.de", + ], + [ + "indeed.com", + + "indeed.ae", + "indeed.ca", + "indeed.ch", + "indeed.cl", + "indeed.co.in", + "indeed.com.au", + "indeed.com.br", + "indeed.com.co", + "indeed.com.mx", + "indeed.com.my", + "indeed.com.pe", + "indeed.com.ph", + "indeed.com.pk", + "indeed.com.sg", + "indeed.co.uk", + "indeed.co.ve", + "indeed.co.za", + "indeed.de", + "indeed.es", + "indeed.fi", + "indeed.fr", + "indeed.hk", + "indeed.ie", + "indeed.jp", + "indeed.lu", + "indeed.nl", + "indeed.pt", + ], + ["independent.co.uk", "indy100.com"], + [ + "iu.edu", + + "indiana.edu", + "iue.edu", + "iufw.edu", + "iuk.edu", + "iun.edu", + "iupuc.edu", + "iupui.edu", + "iusb.edu", + "ius.edu", + "myiu.org", + ], + [ + "jd.com", + "3.cn", + "360buy.com", + "360buyimg.com", + "7fresh.com", + "baitiao.com", + "caiyu.com", + "chinabank.com.cn", + "jd.co.th", + "jd.hk", + "jd.id", + "jd.ru", + "jdpay.com", + "jdwl.com", + "jdx.com", + "jkcsjd.com", + "joybuy.com", + "joybuy.es", + "ocwms.com", + "paipai.com", + "toplife.com", + "wangyin.com", + "yhd.com", + "yihaodianimg.com", + "yiyaojd.com", + "yizhitou.com", + ], + [ + "jetbrains.com", + + "datalore.io", + "intellij.net", + "jetbrains.dev", + "kotlinconf.com", + "kotlinlang.org", + "ktor.io", + "talkingkotlin.com", + ], + ["jpmorganchase.com", "jpmorgan.com", "chase.com"], + ["jobware.de", "jobware.com", "jobware.net"], + ["jotform.com", "jotfor.ms"], + [ + "kayak.com", + + "kayak.ae", + "kayak.cat", + "kayak.ch", + "kayak.cl", + "kayak.co.id", + "kayak.co.in", + "kayak.co.jp", + "kayak.co.kr", + "kayak.com.ar", + "kayak.com.au", + "kayak.com.br", + "kayak.com.co", + "kayak.com.hk", + "kayak.com.mx", + "kayak.com.my", + "kayak.com.pe", + "kayak.com.ph", + "kayak.com.tr", + "kayak.co.th", + "kayak.co.uk", + "kayak.de", + "kayak.dk", + "kayak.es", + "kayak.eu", + "kayak.fr", + "kayak.ie", + "kayak.it", + "kayak.nl", + "kayak.no", + "kayak.ph", + "kayak.pl", + "kayak.pt", + "kayak.qa", + "kayak.ru", + "kayak.se", + "kayak.sg", + + "checkfelix.com", + "checkfelix.co.uk", + "checkfelix.es", + "checkfelix.fr", + "checkfelix.it", + + "momondo.at", + "momondo.be", + "momondo.by", + "momondo.ca", + "momondo.ch", + "momondo.cl", + "momondo.com", + "momondo.com.ar", + "momondo.com.au", + "momondo.com.br", + "momondo.com.cn", + "momondo.com.co", + "momondo.com.pe", + "momondo.com.tr", + "momondo.co.nz", + "momondo.co.uk", + "momondo.co.za", + "momondo.cz", + "momondo.de", + "momondo.dk", + "momondo.ee", + "momondo.es", + "momondo.fi", + "momondo.fr", + "momondogroup.com", + "momondo.hk", + "momondo.ie", + "momondo.in", + "momondo.it", + "momondo.kz", + "momondo.lt", + "momondo.mx", + "momondo.net", + "momondo.nl", + "momondo.no", + "momondo.pl", + "momondo.pro", + "momondo.pt", + "momondo.ro", + "momondo.ru", + "momondo.se", + "momondo.tw", + "momondo.ua", + + "mundi.com.br", + + "speedfares.com", + + "swoodoo.at", + "swoodoo.ch", + "swoodoo.com", + + "r9cdn.net", + ], + ["kiwi.com", "skypicker.com"], + [ + "kogan.com", + + "dicksmith.com.au", + "dicksmith.co.nz", + "koganinternet.com.au", + "koganmobile.co.nz", + "kogansuper.com.au", + "kogantravel.com", + "mattblatt.com.au", + "tandy.com.au", + "zazz.com.au", + ], + ["linkedin.com", "licdn.com"], + ["livejournal.com", "livejournal.net", "lj-toys.com"], + ["lnk.to", "tix.to", "tck.to", "ticket.to", "linkfire.com", "assetlab.io", "linkfire.co", "lnkfi.re"], + [ + "logmeininc.com", + + "citrixonline.com", + "gotomeeting.com", + "gotomeet.me", + "gotomypc.com", + "gotostage.com", + "gotowebinar.com", + "logme.in", + "logmein.com", + + "getgo.com", + ], + [ + "loveholidays.com", + "loveholidays.be", + "loveholidays.dk", + "loveholidays.es", + "loveholidays.fi", + "loveholidays.fr", + "loveholidays.ie", + "loveholidays.nl", + "loveholidays.no", + "loveholidays.co.nz", + "loveholidays.pt", + "loveholidays.se", + "lovevacations.com", + ], + ["macys.com", "macysassets.com"], + [ + "mafra.cz", + + "idnes.cz", + "lidovky.cz", + "expres.cz", + + "1gr.cz", + ], + [ + "mail.ru", + "imgsmail.ru", + + "ok.ru", + "mycdn.me", + "odnoklassniki.ru", + "oklive.app", + "ok.me", + "tamtam.chat", + "tt.me", + + "vk.com", + "vk.me", + "vkontakte.ru", + ], + ["mandtbank.com", "mtb.com"], + ["mathletics.com", "mathletics.com.au", "mathletics.co.uk"], + ["mdsol.com", "imedidata.com"], + [ + "mediamarktsaturn.com", + + "mediamarkt.at", + "mediamarkt.be", + "mediamarkt.ch", + "mediamarkt.com.tr", + "mediamarkt.de", + "mediamarkt.es", + "mediamarkt.gr", + "mediamarkt.hu", + "mediamarkt.nl", + "mediamarkt.se", + + "saturn.at", + "saturn.de", + "saturn.lu", + + "redblue.de", + + "mediamarkt.pl", + "redcoon.pl", + "saturn.pl", + "ms-online.pl", + ], + ["meetup.com", "meetupstatic.com"], + [ + "mercadolibre.com", + + "mercadolibre.cl", + "mercadolibre.co.cr", + "mercadolibre.com.ar", + "mercadolibre.com.bo", + "mercadolibre.com.co", + "mercadolibre.com.do", + "mercadolibre.com.ec", + "mercadolibre.com.gt", + "mercadolibre.com.hn", + "mercadolibre.com.mx", + "mercadolibre.com.ni", + "mercadolibre.com.pa", + "mercadolibre.com.pe", + "mercadolibre.com.py", + "mercadolibre.com.sv", + "mercadolibre.com.uy", + "mercadolibre.com.ve", + "mercadolivre.com", + "mercadolivre.com.br", + + "mercadopago.com", + "mercadopago.com.ar", + "mercadopago.com.br", + "mercadopago.com.co", + "mercadopago.com.mx", + + "mercadoshops.com", + "mercadoshops.cl", + "mercadoshops.com.ar", + "mercadoshops.com.br", + "mercadoshops.com.co", + "mercadoshops.com.mx", + "mercadoshops.com.ve", + + "mercadoclics.com", + "mlstatic.com", + ], + [ + "mercedes-benz.com", + + "mercedes-benz-africa.com", + "mercedes-benz-asia.com", + "mercedes-benz.at", + "mercedes-benz.ba", + "mercedes-benz.be", + "mercedes-benz.bg", + "mercedes-benz.ca", + "mercedes-benz.ch", + "mercedes-benz.cl", + "mercedes-benz.co.id", + "mercedes-benz.co.in", + "mercedes-benz.co.jp", + "mercedes-benz.co.kr", + "mercedes-benz.com.ar", + "mercedes-benz.com.au", + "mercedes-benz.com.br", + "mercedes-benz.com.cn", + "mercedes-benz.com.co", + "mercedes-benz.com.cy", + "mercedes-benz.com.eg", + "mercedes-benz.com.gt", + "mercedes-benz.com.hk", + "mercedes-benz.com.lk", + "mercedes-benz.com.mt", + "mercedes-benz.com.mx", + "mercedes-benz.com.my", + "mercedes-benz.com.pe", + "mercedes-benz.com.ph", + "mercedes-benz.com.sg", + "mercedes-benz.com.tr", + "mercedes-benz.com.tt", + "mercedes-benz.com.tw", + "mercedes-benz.com.uy", + "mercedes-benz.com.vn", + "mercedes-benz.co.nz", + "mercedes-benz.co.th", + "mercedes-benz.co.uk", + "mercedes-benz.co.ve", + "mercedes-benz.co.za", + "mercedes-benz.cz", + "mercedes-benz.de", + "mercedes-benz.dk", + "mercedes-benz-eastern-europe.com", + "mercedes-benz.ee", + "mercedes-benz.es", + "mercedes-benz.fi", + "mercedes-benz.fr", + "mercedes-benz.gr", + "mercedes-benz.hr", + "mercedes-benz.hu", + "mercedes-benz.ie", + "mercedes-benz.is", + "mercedes-benz.it", + "mercedes-benz.li", + "mercedes-benz.lt", + "mercedes-benz.lu", + "mercedes-benz.lv", + "mercedes-benz-mena.com", + "mercedes-benz.nl", + "mercedes-benz.no", + "mercedes-benz-north-cyprus.com", + "mercedes-benz.pl", + "mercedes-benz.pt", + "mercedes-benz.ro", + "mercedes-benz.rs", + "mercedes-benz.ru", + "mercedes-benz.se", + "mercedes-benz.si", + "mercedes-benz.sk", + "mercedes-benz.ua", + ], + ["mi.com", "xiaomi.com"], + [ + "microsoft.com", + + "1drv.ms", + "aadrm.com", + "acompli.net", + "adbureau.net", + "adecn.com", + "aka.ms", + "aquantive.com", + "aspnetcdn.com", + "assets-yammer.com", + "azure.com", + "azureedge.net", + "azure.net", + "azurerms.com", + "bing.com", + "bing.net", + "cloudappsecurity.com", + "dynamics.com", + "gamesforwindows.com", + "getgamesmart.com", + "gfx.ms", + "healthvault.com", + "hockeyapp.net", + "hotmail.com", + "ieaddons.com", + "iegallery.com", + "live.com", + "live.net", + "lync.com", + "microsoftalumni.com", + "microsoftalumni.org", + "microsoftazuread-sso.com", + "microsoftedgeinsiders.com", + "microsoftonline.com", + "microsoftonline-p.com", + "microsoftonline-p.net", + "microsoftstore.com", + "microsoftstream.com", + "msads.net", + "msappproxy.net", + "msauthimages.net", + "msecnd.net", + "msedge.net", + "msftidentity.com", + "msft.net", + "msidentity.com", + "msn.com", + "msndirect.com", + "msocdn.com", + "netconversions.com", + "o365weve.com", + "oaspapps.com", + "office365.com", + "office.com", + "officelive.com", + "office.net", + "olsvc.com", + "onedrive.com", + "onenote.com", + "onenote.net", + "onestore.ms", + "onmicrosoft.com", + "outlook.com", + "outlookmobile.com", + "passport.net", + "phonefactor.net", + "powerapps.com", + "roiservice.com", + "sfbassets.com", + "sfx.ms", + "sharepoint.com", + "sharepoint-df.com", + "sharept.ms", + "skypeassets.com", + "skype.com", + "skypeforbusiness.com", + "s-microsoft.com", + "s-msn.com", + "staffhub.ms", + "svc.ms", + "sway-cdn.com", + "sway.com", + "sway-extensions.com", + "trafficmanager.net", + "virtualearth.net", + "visualstudio.com", + "vsallin.net", + "vsassets.io", + "windowsazure.com", + "windows.com", + "windows.net", + "windowsphone.com", + "worldwidetelescope.org", + "wunderlist.com", + "xbox.com", + "xboxlive.com", + "yammer.com", + "yammerusercontent.com", + + "github.com", + "githubapp.com", + "githubassets.com", + "github.dev", + + "avatars0.githubusercontent.com", + "avatars1.githubusercontent.com", + "avatars2.githubusercontent.com", + "avatars3.githubusercontent.com", + "camo.githubusercontent.com", + "cloud.githubusercontent.com", + "raw.githubusercontent.com", + ], + ["mobilism.org.in", "mobilism.org"], + ["morganstanley.com", "morganstanleyclientserv.com", "stockplanconnect.com", "ms.com"], + [ + "morningstar.com", + + "morningstar.at", + "morningstar.be", + "morningstarbr.com", + "morningstar.ca", + "morningstar.ch", + "morningstar.cl", + "morningstar.co.il", + "morningstar.com.mx", + "morningstar.co.uk", + "morningstar.de", + "morningstar.dk", + "morningstar.es", + "morningstar.fi", + "morningstar.fr", + "morningstarfunds.ie", + "morningstar.it", + "morningstar.nl", + "morningstar.no", + "morningstar.pt", + "morningstar.se", + "morningstarthailand.com", + ], + [ + "mtv.fi", + + "cmore.fi", + "lumijapyry.fi", + "luukku.com", + "mtvuutiset.fi", + "salatutelamat.fi", + "studio55.fi", + "suomiareena.fi", + ], + ["my-bookings.org", "my-bookings.cc"], + [ + "myheritage.com", + + "myheritageadn.be", + "myheritageadn.fr", + "myheritageadn.it", + "myheritageadn.pt", + "myheritage.am", + "myheritage.at", + "myheritage.be", + "myheritage.cat", + "myheritage.ch", + "myheritage.cn", + "myheritage.co.il", + "myheritage.co.in", + "myheritage.co.kr", + "myheritage.com.br", + "myheritage.com.hr", + "myheritage.com.pt", + "myheritage.com.tr", + "myheritage.com.ua", + "myheritage.cz", + "myheritage.de", + "myheritage.dk", + "myheritagedna.be", + "myheritagedna.com", + "myheritagedna.fr", + "myheritagedna.it", + "myheritagedna.pt", + "myheritage.ee", + "myheritage.es", + "myheritage.fi", + "myheritage.fr", + "myheritage.gr", + "myheritage.hu", + "myheritage.it", + "myheritage.jp", + "myheritagelibraryedition.com", + "myheritage.lt", + "myheritage.lv", + "myheritage.mk", + "myheritage.nl", + "myheritage.no", + "myheritage.pl", + "myheritage.pt", + "myheritage.ro", + "myheritage.rs", + "myheritage.se", + "myheritage.si", + "myheritage.sk", + "myheritage.tw", + + "dnaquest.org", + "familygraph.com", + "familygraphql.com", + "familytreebuilder.com", + "tribalquest.org", + + "mhcache.com", + "myheritage-container.com", + "myheritagefiles.com", + "myheritageimages.com", + ], + ["mymerrill.com", "ml.com", "merrilledge.com"], + ["mynortonaccount.com", "norton.com"], + ["mysmartedu.com", "mysmartabc.com"], + ["myuv.com", "uvvu.com"], + [ + "naver.com", + + "grafolio.com", + "plug.game", + "vlive.tv", + "webtoons.com", + + "naver.net", + "pstatic.net", + + "blog.jp", + "blogos.com", + "doorblog.jp", + "ldblog.jp", + "linecorp.com", + "line.me", + "livedoor.com", + "livedoor.jp", + + "blogcms.jp", + "blogimg.jp", + "blogsys.jp", + "line-apps.com", + "line.biz", + "line-scdn.net", + "livedoor.net", + "naver.jp", + ], + [ + "nbcnews.com", + + "msnbc.com", + "today.com", + + "newsvine.com", + "s-nbcnews.com", + ], + ["nefcuonline.com", "nefcu.com"], + [ + "netease.com", + + "126.com", + "126.net", + "127.net", + "163.com", + + "icourse163.org", + "kada.com", + "kaola.com", + "kaola.com.hk", + ], + ["netflix.com", "nflxext.com", "nflximg.net", "nflxvideo.net"], + [ + "nettix.fi", + + "nettiauto.com", + "nettikaravaani.com", + "nettikone.com", + "nettimarkkina.com", + "nettimokki.com", + "nettimoto.com", + "nettivaraosa.com", + "nettivene.com", + "nettivuokraus.com", + ], + ["newegg.com", "neweggbusiness.com", "neweggimages.com", "newegg.ca"], + [ + "newscorpaustralia.com", + + "1degree.com.au", + "adelaidenow.com.au", + "api.news", + "bestrecipes.com.au", + "bodyandsoul.com.au", + "brisbanenews.com.au", + "cairnspost.com.au", + "couriermail.com.au", + "dailytelegraph.com.au", + "delicious.com.au", + "escape.com.au", + "foxsports.com.au", + "geelongadvertiser.com.au", + "goldcoastbulletin.com.au", + "gq.com.au", + "heraldsun.com.au", + "homelife.com.au", + "insideout.com.au", + "kidspot.com.au", + "nativeincolour.com.au", + "newsadds.com.au", + "newsapi.com.au", + "newscdn.com.au", + "news.com.au", + "news.net.au", + "newsprestigenetwork.com.au", + "newsxtend.com.au", + "nlm.io", + "ntnews.com.au", + "supercoach.com.au", + "taste.com.au", + "theaustralian.com.au", + "themercury.com.au", + "townsvillebulletin.com.au", + "vogue.com.au", + "weeklytimesnow.com.au", + "whereilive.com.au", + "whimn.com.au", + ], + [ + "nintendo.com", + "nintendo.net", + "nintendo-europe.com", + "nintendonyc.com", + + "nintendo.at", + "nintendo.be", + "nintendo.ch", + "nintendo.co.uk", + "nintendo.co.za", + "nintendo.de", + "nintendo.es", + "nintendo.eu", + "nintendo.fr", + "nintendo.it", + "nintendo.nl", + "nintendo.pt", + "nintendo.ru", + + "animal-crossing.com", + "smashbros.com", + "zelda.com", + ], + ["norsk-tipping.no", "buypass.no"], + [ + "npo.nl", + + "2doc.nl", + "3fm.nl", + "avrotros.nl", + "bnnvara.nl", + "brainwash.nl", + "delagarde.nl", + "eo.nl", + "funx.nl", + "human.nl", + "jeugdjournaal.nl", + "joop.nl", + "kro-ncrv.nl", + "kro.nl", + "npo3fm.nl", + "npo3.nl", + "npoplus.nl", + "nporadio1.nl", + "nporadio2.nl", + "nporadio4.nl", + "nporadio5.nl", + "npostart.nl", + "ntr.nl", + "omroep.nl", + "powned.tv", + "publiekeomroep.nl", + "radio4.nl", + "schooltv.nl", + "vara.nl", + "vpro.nl", + "zappelin.nl", + "zapp.nl", + ], + ["nymag.com", "vulture.com", "grubstreet.com", "thecut.com"], + [ + "nypublicradio.org", + + "newsounds.org", + "radiolab.org", + "thegreenespace.org", + "wnycstudios.org", + "wqxr.org", + + "wnyc.org", + ], + ["nytimes.com", "newyorktimes.com", "thewirecutter.com", "nyt.com"], + ["nyu.edu", "nyupress.org"], + [ + "nvidia.com", + + "nvidia.at", + "nvidia.be", + "nvidia.ch", + "nvidia.cn", + "nvidia.co.at", + "nvidia.co.in", + "nvidia.co.jp", + "nvidia.co.kr", + "nvidia.com.au", + "nvidia.com.br", + "nvidia.com.mx", + "nvidia.com.pe", + "nvidia.com.pl", + "nvidia.com.tr", + "nvidia.com.tw", + "nvidia.com.ua", + "nvidia.com.ve", + "nvidia.co.uk", + "nvidia.cz", + "nvidia.de", + "nvidia.dk", + "nvidia.es", + "nvidia.eu", + "nvidia.fi", + "nvidia.fr", + "nvidia.in", + "nvidia.it", + "nvidia.jp", + "nvidia.lu", + "nvidia.mx", + "nvidia.nl", + "nvidia.no", + "nvidia.pl", + "nvidia.ro", + "nvidia.ru", + "nvidia.se", + "nvidia.tw", + + "nvidiagrid.net", + "nvidia.partners", + + "geforce.com", + "geforcenow.com", + "gputechconf.com", + + "geforce.cn", + "geforce.com.tw", + "geforce.co.uk", + ], + ["onlineatnsb.com", "norwaysavingsbank.com"], + ["openstreetmap.org", "osmfoundation.org"], + [ + "oracle.com", + + "ateam-oracle.com", + "java.com", + "mysql.com", + + "oracleimg.com", + ], + ["orange.fr", "sosh.fr", "woopic.com"], + [ + "osf.io", + + "agrixiv.org", + "arabixiv.org", + "eartharxiv.org", + "ecsarxiv.org", + "engrxiv.org", + "frenxiv.org", + "marxiv.org", + "mindrxiv.org", + "paleorxiv.org", + "psyarxiv.com", + "thesiscommons.org" + ], + ["osu.edu", "osumc.edu", "ohio-state.edu"], + [ + "ovh.com", + + "kimsufi.com", + "ovhcloud.com", + "ovhtelecom.fr", + "soyoustart.com", + + "ovh.com.au", + "ovh.co.uk", + "ovh.cz", + "ovh.de", + "ovh.es", + "ovh-hosting.fi", + "ovh.ie", + "ovh.it", + "ovh.lt", + "ovh.nl", + "ovh.pl", + "ovh.pt", + "ovh.sn", + + "ovh.net", + ], + ["paypal.com", "paypal-search.com", "paypalobjects.com"], + ["pcworld.com", "staticworld.net", "idg.com", "idg.net", "infoworld.com", "macworld.com", "techhive.com", "idg.tv"], + [ + "pearson.com", + + "connexus.com", + "ecollege.com", + "english.com", + "masteringchemistry.com", + "masteringengineering.com", + "masteringgeography.com", + "masteringhealthandnutrition.com", + "masteringphysics.com", + "mathxl.com", + "mathxlforschool.com", + "mypearson.com", + "pearsonassessments.com", + "pearsoned.com", + "pearsonelt.com", + "pearsonhighered.com", + "pearsonmylabandmastering.com", + + "pearsoncmg.com", + ], + ["pepco.com", "pepcoholdings.com"], + ["philips.com", "philips.nl"], + [ + "pinterest.com", + + "pinterest.at", + "pinterest.be", + "pinterest.ca", + "pinterest.ch", + "pinterest.cl", + "pinterest.co", + "pinterest.co.at", + "pinterest.co.in", + "pinterest.co.kr", + "pinterest.com.au", + "pinterest.com.bo", + "pinterest.com.ec", + "pinterest.com.mx", + "pinterest.com.pe", + "pinterest.com.py", + "pinterest.com.uy", + "pinterest.com.vn", + "pinterest.co.nz", + "pinterest.co.uk", + "pinterest.de", + "pinterest.dk", + "pinterest.ec", + "pinterest.engineering", + "pinterest.es", + "pinterest.fr", + "pinterest.hu", + "pinterest.id", + "pinterest.ie", + "pinterest.in", + "pinterest.info", + "pinterest.it", + "pinterest.jp", + "pinterest.kr", + "pinterestmail.com", + "pinterest.mx", + "pinterest.nz", + "pinterest.pe", + "pinterest.ph", + "pinterest.pt", + "pinterest.ru", + "pinterest.se", + "pinterest.th", + "pinterest.tw", + "pinterest.uk", + "pinterest.vn", + + "pinimg.com", + "pin.it", + ], + ["plex.tv", "plex.direct"], + ["pokemon-gl.com", "pokemon.com"], + ["pornhub.com", "phncdn.com"], + ["postepay.it", "poste.it"], + ["postimees.ee", "city24.ee", "city24.lv", "pmo.ee"], + [ + "pricerunner.com", + + "pricerunner.co.uk", + "pricerunner.de", + "pricerunner.dk", + "pricerunner.net", + "pricerunner.se", + "pricerunner.uk", + ], + [ + "prosiebensat1.de", + "prosiebensat1.com", + + "atv.at", + "atv2.at", + "galileo.tv", + "kabeleins.at", + "kabeleins.ch", + "kabeleins.de", + "kabeleinsdoku.at", + "kabeleinsdoku.ch", + "kabeleinsdoku.de", + "prosieben.at", + "prosieben.ch", + "prosieben.de", + "prosiebenmaxx.at", + "prosiebenmaxx.ch", + "prosiebenmaxx.de", + "puls24.at", + "puls4.com", + "puls8.ch", + "ran.de", + "sat1.at", + "sat1.ch", + "sat1.de", + "sat1gold.at", + "sat1gold.ch", + "sat1gold.de", + "sixx.at", + "sixx.ch", + "sixx.de", + "the-voice-of-germany.at", + "the-voice-of-germany.ch", + "the-voice-of-germany.de", + "zappn.tv", + + "p7s1.io", + ], + [ + "qantas.com", + + "jetstar.com", + "qantas.com.au", + "qantascourier.com.au", + "qantasfutureplanet.com.au", + "qantasgrouptravel.com", + "qfcrew.com", + "qfflightcrew.com", + + "aquire.com.au", + "qantasassure.com", + "qantasbusinessrewards.com", + "qantasbusinessrewards.com.au", + "qantasepiqure.com", + "qantasepiqure.com.au", + "qantasgolfclub.com", + "qantasgolfclub.com.au", + "qantasloyalty.com", + "qantasloyalty.net", + "qantasmall.com", + "qantasmall.com.au", + "qantasmall.co.nz", + "qantaspoints.com", + "qantaspoints.com.au", + "qantasshopping.com", + "qantasshopping.com.au", + "qantasshopping.co.nz", + "qantasstore.com.au", + "qantasstore.co.nz", + "redplanetgroup.com.au", + "redplanetportal.com.au", + + "qantascash.com", + "qantascash.com.au", + "qantasmoney.com", + "qantastravelmoney.com", + ], + [ + "qq.com", + + "aitangyou.com", + "cdntips.com", + "dnspod.cn", + "extqq.com", + "gdtimg.com", + "gtimg.cn", + "gtimg.com", + "idqqimg.com", + "imqq.com", + "myapp.com", + "myqcloud.com", + "qcloud.com", + "qpic.cn", + "qqmail.com", + "qzone.com", + "tencent.com", + "tenpay.com", + "ugdtimg.com", + "url.cn", + "wechat.com", + "wegame.com", + "weiyun.com", + ], + [ + "rai.it", + + "comunitaitalofona.org", + "raicinema.it", + "raicultura.it", + "raimemo.it", + "rainews24.it", + "rainews.it", + "raiplay.it", + "raiplayradio.it", + "raiplayyoyo.it", + "raipubblicita.it", + "raisport.it", + "raitalia.it", + "rai.tv", + "raiway.it", + ], + ["railnation.ru", "railnation.de", "rail-nation.com", "railnation.gr", "railnation.us", "trucknation.de", "traviangames.com"], + ["rakuten.com", "buy.com"], + [ + "realestate.com.au", + + "property.com.au", + "realcommercial.com.au", + "spacely.com.au", + + "reastatic.net", + ], + ["reddit.com", "redditmedia.com", "redditstatic.com", "redd.it", "redditenhancementsuite.com", "reddituploads.com", "imgur.com"], + ["redhat.com", "openshift.com", "openshift.org", "okd.io"], + [ + "reebok.at", + "reebok.be", + "reebok.ca", + "reebok.ch", + "reebok.cl", + "reebok.co", + "reebok.com", + "reebok.com.ar", + "reebok.com.br", + "reebok.com.tr", + "reebok.co.uk", + "reebok.cz", + "reebok.de", + "reebok.dk", + "reebok.es", + "reebok.fi", + "reebok.fr", + "reebok.ie", + "reebok.it", + "reebok.mx", + "reebok.nl", + "reebok.pe", + "reebok.pl", + "reebok.ru", + "reebok.se", + "reebok.sk", + ], + [ + "reuters.com", + "reuters.tv", + "reutersmedia.net", + "thomsonreuters.com", + + "reutersagency.cn", + + "thomsonreuters.ca", + "thomsonreuters.cn", + "thomsonreuters.co.jp", + "thomsonreuters.co.kr", + "thomsonreuters.com.ar", + "thomsonreuters.com.au", + "thomsonreuters.com.br", + "thomsonreuters.com.hk", + "thomsonreuters.com.my", + "thomsonreuters.com.pe", + "thomsonreuters.com.sg", + "thomsonreuters.com.tr", + "thomsonreuters.co.uk", + "thomsonreuters.es", + "thomsonreuters.in", + "thomsonreuters.ru", + ], + [ + "riotgames.com", + + "leagueoflegends.com", + "lolesports.com", + "lolstatic.com", + "lolusercontent.com", + + "playruneterra.com", + + "riotcdn.net", + "rdatasrv.net", + ], + [ + "rtl.nl", + + "bright.nl", + "buienradar.nl", + "healthyfest.nl", + "rtlboulevard.nl", + "rtllatenight.nl", + "rtlnieuws.nl", + "rtlxl.nl", + "rtlz.nl", + "videoland.com", + "vtbl.nl", + ], + [ + "s-kanava.fi", + + "abcasemat.fi", + "raflaamo.fi", + "s-mobiili.fi", + "sokoshotels.fi", + "yhteishyva.fi", + + "sok.fi", + "s-palvelut.fi", + ], + [ + "salesforce.com", + + "documentforce.com", + "einstein.com", + "force.com", + "pardot.com", + "salesforceliveagent.com", + "visualforce.com", + ], + ["sanguosha.com", "bianfeng.com"], + ["schwab.com", "schwabplan.com"], + ["scmp.com", "i-scmp.com"], + ["sears.com", "shld.net"], + [ + "seznam.cz", + + "firmy.cz", + "garaz.cz", + "kupi.cz", + "lide.cz", + "mapy.cz", + "novinky.cz", + "prozeny.cz", + "sauto.cz", + "sbazar.cz", + "sdovolena.cz", + "seznamzpravy.cz", + "sport.cz", + "sreality.cz", + "stream.cz", + "super.cz", + "sweb.cz", + "televizeseznam.cz", + "volnamista.cz", + "zbozi.cz", + + "szn.cz", + ], + [ + "shopify.com", + "myshopify.com", + "shopifycdn.com", + "shopifyapps.com", + "shopifycloud.com", + "shopifyadmin.com", + "shopifypreview.com", + ], + ["siriusxm.com", "sirius.com"], + ["skygo.co.nz", "skytv.co.nz"], + ["skysports.com", "skybet.com", "skyvegas.com"], + ["slashdot.org", "sourceforge.net", "fsdn.com"], + ["slickdeals.net", "slickdealscdn.com"], + [ + "smh.com.au", + + "afr.com", + "brisbanetimes.com.au", + "canberratimes.com.au", + "fairfaxmedia.com.au", + "theage.com.au", + "watoday.com.au", + + "ffx.io", + ], + ["snapfish.com", "snapfish.ca"], + [ + "sony.com", + + "sonyentertainmentnetwork.com", + "sonyrewards.com", + + "playstation.com", + "playstation.net", + + "sony-africa.com", + "sony-asia.com", + "sony.at", + "sony.ba", + "sony.be", + "sony.bg", + "sony.ca", + "sony.ch", + "sony.cl", + "sony.co.cr", + "sony.co.id", + "sony.co.in", + "sony.co.kr", + "sony.com.ar", + "sony.com.au", + "sony.com.bo", + "sony.com.br", + "sony.com.co", + "sony.com.do", + "sony.com.ec", + "sony.com.gt", + "sony.com.hk", + "sony.com.hn", + "sony.com.mk", + "sony.com.mx", + "sony.com.my", + "sony.com.ni", + "sony.com.pa", + "sony.com.pe", + "sony.com.ph", + "sony.com.sg", + "sony.com.sv", + "sony.com.tr", + "sony.com.tw", + "sony.com.vn", + "sony.co.nz", + "sony.co.th", + "sony.co.uk", + "sony.cz", + "sony.de", + "sony.dk", + "sony.ee", + "sony.es", + "sony.eu", + "sony-europe.com", + "sony.fi", + "sony.fr", + "sony.gr", + "sony.hr", + "sony.hu", + "sony.ie", + "sony.it", + "sony.kz", + "sony-latin.com", + "sonylatvija.com", + "sony.lt", + "sony.lu", + "sony.lv", + "sony-mea.com", + "sony.nl", + "sony.no", + "sony.pl", + "sony-promotion.eu", + "sony.pt", + "sony.ro", + "sony.rs", + "sony.ru", + "sony.se", + "sony.si", + "sony.sk", + "sony.ua", + + "sony.net", + ], + ["soundcloud.com", "sndcdn.com"], + ["soundcu.com", "netteller.com"], + ["southerncompany.com", "southernco.com"], + ["southparkstudios.com", "cc.com", "comedycentral.com"], + ["spiceworks.com", "spiceworksstatic.com"], + [ + "spotify.com", + + "scdn.co", + "spotifyforbrands.com", + "spotifyforartists.com", + "spotify.net", + ], + [ + "springernature.com", + + "adis.com", + "apress.com", + "biomedcentral.com", + "bsl.nl", + "dgim-eakademie.de", + "kardiologie.org", + "macmillaneducation.com", + "macmillanexplorers.com", + "medengine.com", + "medicinematters.com", + "medicinematters.in", + "medwirenews.com", + "metzlerverlag.de", + "nature.com", + "natureindex.com", + "palgrave.com", + "scientificamerican.com", + "springer.com", + "springeraesthetik.de", + "springerhealthcare.com", + "springermedizin.at", + "springermedizin.de", + "springeropen.com", + "springerpflege.de", + "springerprofessional.de", + ], + ["sprint.com", "sprintpcs.com", "nextel.com"], + ["squareup.com", "cash.app", "mkt.com", "squarecdn.com"], + ["steampowered.com", "steamstatic.com", "steamcommunity.com"], + ["suning.com", "suning.cn", "hksuning.com"], + ["target.com", "targetimg1.com"], + ["techdata.com", "techdata.ch"], + ["telegram.org", "telegram.me", "t.me"], + ["telekom.com", "t-online.de"], + ["tesla.com", "teslamotors.com"], + [ + "toyota.com", + + "lexus.com", + + "toyota.am", + "toyota.at", + "toyota.az", + "toyota.ba", + "toyota.be", + "toyota.bg", + "toyota-canarias.es", + "toyotacg.me", + "toyota.ch", + "toyota.co.il", + "toyota.com.cy", + "toyota.com.mk", + "toyota.com.mt", + "toyota.com.tr", + "toyota.co.uk", + "toyota.cz", + "toyota.de", + "toyota.dk", + "toyota.ee", + "toyota.es", + "toyota.fi", + "toyota.fr", + "toyota.ge", + "toyota-gib.com", + "toyota.gr", + "toyota.hr", + "toyota.hu", + "toyota.ie", + "toyota.is", + "toyota.it", + "toyota-kosovo.com", + "toyota.kz", + "toyota.lt", + "toyota.lu", + "toyota.lv", + "toyota.md", + "toyota.nl", + "toyota.no", + "toyota.pl", + "toyota.pt", + "toyota.ro", + "toyota.rs", + "toyota.ru", + "toyota.se", + "toyota.si", + "toyota.sk", + "toyota.ua", + + "toyota-europe.com", + ], + [ + "tripadvisor.com", + + "tripadvisor.at", + "tripadvisor.be", + "tripadvisor.ca", + "tripadvisor.ch", + "tripadvisor.co.hu", + "tripadvisor.co.id", + "tripadvisor.com.ar", + "tripadvisor.com.au", + "tripadvisor.com.br", + "tripadvisor.com.gr", + "tripadvisor.com.hk", + "tripadvisor.com.mx", + "tripadvisor.com.my", + "tripadvisor.com.pe", + "tripadvisor.com.ph", + "tripadvisor.com.sg", + "tripadvisor.com.tr", + "tripadvisor.com.tw", + "tripadvisor.co.nz", + "tripadvisor.co.uk", + "tripadvisor.co.za", + "tripadvisor.de", + "tripadvisor.dk", + "tripadvisor.es", + "tripadvisor.fi", + "tripadvisor.fr", + "tripadvisor.ie", + "tripadvisor.in", + "tripadvisor.it", + "tripadvisor.jp", + "tripadvisor.nl", + "tripadvisor.pt", + "tripadvisor.ru", + "tripadvisor.se", + "tripadvisor.sk", + + "seatguru.com", + + "tacdn.com", + "tamgrt.com", + ], + [ + "trivago.com", + + "trivago.ae", + "trivago.at", + "trivago.be", + "trivago.bg", + "trivago.ca", + "trivago.ch", + "trivago.cl", + "trivago.co.id", + "trivago.co.il", + "trivago.co.kr", + "trivago.com.ar", + "trivago.com.au", + "trivago.com.br", + "trivago.com.co", + "trivago.com.ec", + "trivago.com.mx", + "trivago.com.my", + "trivago.com.ph", + "trivago.com.tr", + "trivago.com.tw", + "trivago.com.uy", + "trivago.co.nz", + "trivago.co.th", + "trivago.co.uk", + "trivago.co.za", + "trivago.cz", + "trivago.dk", + "trivago.es", + "trivago.fi", + "trivago.fr", + "trivago.gr", + "trivago.hk", + "trivago.hr", + "trivago.hu", + "trivago.ie", + "trivago.in", + "trivago.it", + "trivago.jp", + "trivago.nl", + "trivago.no", + "trivago.pe", + "trivago.pl", + "trivago.pt", + "trivago.ro", + "trivago.rs", + "trivago.ru", + "trivago.se", + "trivago.sg", + "trivago.si", + "trivago.sk", + "trivago.vn", + "youzhan.com", + ], + ["trsretire.com", "divinvest.com"], + ["turbotax.com", "intuit.com"], + [ + "tvn.pl", + + "player.pl", + "tvn24bis.pl", + "tvn24.pl", + + "cdntvn.pl", + ], + ["tvp.pl", "tvp.info"], + ["twitter.com", "twimg.com", "t.co", "periscope.tv", "pscp.tv"], + ["ua2go.com", "ual.com", "united.com", "unitedwifi.com"], + ["ubisoft.com", "ubi.com", "anno-union.com", "thesettlers-alliance.com"], + ["ui.com", "ubnt.com"], + [ + "unitedhealthgroup.com", + + "myprotectwell.com", + "protectwellapp.com", + "protectwell.org", + "uhg.com", + "unitedhealthgroup.net", + "unitedhealthgroup.org", + "weprotectwell.com", + + "healthyourway.com", + "healthyourwaynow.com", + "joinatyourbest.com", + "mypersonalizedsupport.com", + "myrenewactive.com", + "mywellbeingsolution.com", + "optummessenger.co", + "personalhealthmessagecenter.com", + "phs.com", + "pwrfitness.com", + "takechargeatwork.com", + "wellnesscoachingnow.com", + + "aarpmedicareplans.com", + "careimprovementplus.com", + "health4me.com", + "healthsafe-id.com", + "myaarpmedicare.com", + "myaarpmedicareplans.com", + "myaarprenew.com", + "myaarprenewma.com", + "myaarprenewmapd.com", + "myaarprenewpdp.com", + "myaarpsupplementalhealthinsurance.com", + "mymedicamedicare.com", + "mymedicareaccount.com", + "mypcpmedicare.com", + "myuhc.com", + "myuhcdental.com", + "myuhcmedicare.com", + "uhc.com", + "uhcmedicaresolutions.com", + "uhcmycarepath.com", + "uhcretiree.com", + "uhcservices.com", + "yourdentalplan.com", + + "werally.com", + + "liveandworkwell.com", + "myoptum.com", + "optum.com", + "optumrx.com", + "ppconline.info", + "ppconlineinfo.com", + "wellbeing-4life.com", + ], + ["vanguard.com", "investornews.vanguard", "vanguardblog.com", "vanguardcanada.ca", "vanguardinvestor.co.uk", "vgcontent.info"], + [ + "verizonmedia.com", + + "verizon.com", + "verizon.net", + "verizonwireless.com", + "vzw.com", + + "aol.com", + "autoblog.com", + "engadget.com", + "oath.com", + "overture.com", + "techcrunch.com", + "yahoo.com", + + "huffpost.com", + "huffingtonpost.ca", + "huffingtonpost.com", + "huffingtonpost.com.au", + "huffingtonpost.com.mx", + "huffingtonpost.co.uk", + "huffingtonpost.co.za", + "huffingtonpost.de", + "huffingtonpost.es", + "huffingtonpost.fr", + "huffingtonpost.gr", + "huffingtonpost.in", + "huffingtonpost.it", + "huffingtonpost.jp", + "huffingtonpost.kr", + "huffpostarabi.com", + "huffpostbrasil.com", + "huffpostmaghreb.com", + + "yahooapis.com", + "yimg.com", + ], + ["vimeo.com", "vimeocdn.com"], + [ + "vinted.com", + + "kleiderkreisel.de", + "mamikreisel.de", + + "vinted.co.uk", + "vinted.cz", + "vinted.es", + "vinted.fr", + "vinted.lt", + "vinted.nl", + "vinted.pl", + ], + ["volvooceanrace.com", "virtualregatta.com"], + ["vonage.com", "vonagebusiness.com"], + [ + "vrt.be", + + "canvas.be", + "een.be", + "ketnet.be", + "klara.be", + "mnm.be", + "radio1.be", + "radio2.be", + "sporza.be", + "stubru.be", + ], + ["wa.gov", "wsdot.com", "wsdot.gov"], + ["walmart.com", "wal.co", "walmartimages.com", "walmart.ca"], + [ + "thewaltdisneycompany.com", + + "6abc.com", + "abc7.com", + "abc7ny.com", + "abc.com", + "abcnews.com", + + "go.com", + + "espn.com", + "espncdn.com", + + "espn.com.au", + "espn.com.br", + "espn.co.uk", + + "espncricinfo.com", + + "espnfc.com", + "espnfc.us", + + "fivethirtyeight.com", + + "babble.com", + "dadt.com", + "disneybaby.com", + "disneyinteractive.com", + "disneyinternationalhd.com", + "disneymoviesanywhere.com", + "disneyplus.com", + "ilm.com", + "marvel.com", + "readriordan.com", + "skysound.com", + "starwars.com", + + "disney.asia", + "disney.be", + "disney.bg", + "disney.co.il", + "disney.com", + "disney.com.au", + "disney.com.br", + "disney.com.hk", + "disney.com.tw", + "disney.co.th", + "disney.co.uk", + "disney.co.za", + "disney.cz", + "disney.de", + "disney.dk", + "disney.es", + "disney.fi", + "disney.fr", + "disney.gr", + "disney.hu", + "disney.id", + "disney.in", + "disney.it", + "disneylatino.com", + "disneyme.com", + "disney.my", + "disney.nl", + "disney.no", + "disney.ph", + "disney.pl", + "disney.pt", + "disney.ro", + "disney.se", + "disney.sg", + "disneyturkiye.com.tr", + + "dilcdn.com", + "disney.io", + ], + [ + "wargaming.com", + "wargaming.net", + "worldoftanks.asia", + "worldoftanks.com", + "worldoftanks.eu", + "worldoftanks.kr", + "worldoftanks.ru", + "worldofwarplanes.com", + "worldofwarplanes.eu", + "worldofwarplanes.ru", + "worldofwarships.com", + "worldofwarships.eu", + "worldofwarships.ru", + "wotblitz.com", + ], + [ + "warnermediagroup.com", + + "adultswim.com", + "adventuretime.com", + "atgamewiz.com", + "ben10.com", + "bleacherreport.com", + "bleacherreportlive.com", + "br.live", + "brlive.com", + "brlive.io", + "cartoonnetwork.asia", + "cartoonnetworkasia.com", + "cartoonnetwork.com", + "cartoonnetworkhotel.com", + "cartoonnetworkpr.com", + "catchsports.com", + "chasingthecurelive.com", + "cnn.com", + "cnn.io", + "cnnmoney.ch", + "cnn.net", + "d2c-ott.com", + "d-league.com", + "e-league.com", + "eleague.com", + "filmstruck.com", + "greatbig.com", + "greatbigstory.com", + "hbo.com", + "hbogo.com", + "hbomax.com", + "hbonow.com", + "heaveninc.com", + "hlntv.com", + "iamthenight.com", + "juniorrydercup.com", + "maxgo.com", + "nba.com", + "nba.net", + "ncaa.com", + "ngtv.io", + "penis-map.com", + "pgachampionship.com", + "pga.com", + "pga-events.com", + "pga.net", + "powerpuffgirls.com", + "powerpufftheworld.com", + "robotunicornattack.com", + "rydercup.com", + "samanthabee.com", + "sambee.com", + "shaqtoons.com", + "stevenuniverseselfesteem.com", + "stevenuniversethemovie.com", + "summercampisland.com", + "superstation.com", + "suspensecollection.com", + "tbs.com", + "tcm.com", + "teamcococdn.com", + "thealienist.com", + "thesuspensecollection.com", + "ti-platform.com", + "tntdrama.com", + "tnt.tv", + "trutv.com", + "vgtf.net", + "warnermediacdn.com", + "warnermediafitnation.com", + "warnermediaready.com", + "warnermediasupplierdiversity.com", + "warnermediaupfront.com", + "wbtvd.com", + "webarebears.com", + "wnba.com", + "yzzerdd.com", + + "turner.com", + "ugdturner.com", + ], + [ + "weather.com", + "wunderground.com", + + "imwx.com", + "wfxtriggers.com", + "wsi.com", + "w-x.co", + "wxug.com", + ], + [ + "webmd.com", + + "emedicinehealth.com", + "medicinenet.com", + "medscape.com", + "medscape.org", + "onhealth.com", + "rxlist.com", + + "medscapestatic.com", + ], + ["weebly.com", "editmysite.com"], + [ + "weibo.com", + + "sina.cn", + "sina.com", + "sina.com.cn", + "sinaedge.com", + "sinaimg.cn", + "sinaimg.com", + "sinaimg.com.cn", + "sinajs.cn", + "sina.net", + "wbimg.cn", + "wbimg.com", + "wcdn.cn", + "weibocdn.com", + "weibo.cn", + "weibopay.com", + ], + ["wellsfargo.com", "wf.com"], + ["wetter.com", "tiempo.es", "wettercomassets.com"], + ["wikipedia.org", "wikimedia.org", "wikimediafoundation.org", "wiktionary.org", + "wikiquote.org", "wikibooks.org", "wikisource.org", "wikinews.org", + "wikiversity.org", "mediawiki.org", "wikidata.org", "wikivoyage.org", + "wmfusercontent.org", "tools.wmflabs.org"], + ["wisconsin.gov", "wi.gov"], + ["wix.com", "wixapps.net", "wixstatic.com", "parastorage.com"], + ["wordpress.com", "wp.com"], + [ + "wp.pl", + + "abczdrowie.pl", + "allani.pl", + "autokult.pl", + "dobreprogramy.pl", + "domodi.pl", + "eholiday.pl", + "finansowysupermarket.pl", + "fotoblogia.pl", + "gadzetomania.pl", + "homebook.pl", + "jedenwniosek.pl", + "komorkomania.pl", + "money.pl", + "nocowanie.pl", + "o2.pl", + "open.fm", + "parenting.pl", + "pudelek.pl", + "pudelek.tv", + "totalmoney.pl", + "wakacje.pl", + "wawalove.pl", + "wp.tv", + + "wpcdn.pl", + "wpimg.pl", + ], + ["wpcu.coop", "wpcuonline.com"], + ["wsj.com", "wsj.net", "barrons.com", "dowjones.com", "dowjoneson.com", "mansionglobal.com", "marketwatch.com"], + ["xda-developers.com", "xda-cdn.com"], + ["xfinity.com", "comcast.net", "comcast.com"], + ["xhamster.com", "xhcdn.com"], + ["yahoo.co.jp", "yimg.jp"], + [ + "yandex.com", + + "auto.ru", + "beru.ru", + "bringly.ru", + "kinopoisk.ru", + "mykp.ru", + "yadi.sk", + + "yandex.net", + "yastatic.net", + + "yandex.az", + "yandex.by", + "yandex.co.il", + "yandex.com.am", + "yandex.com.ge", + "yandex.com.tr", + "yandex.com.ua", + "yandex.ee", + "yandex.fr", + "yandex.kg", + "yandex.kz", + "yandex.lt", + "yandex.lv", + "yandex.md", + "yandex.ru", + "yandex.tj", + "yandex.tm", + "yandex.ua", + "yandex.uz", + "ya.ru", + ], + ["yoox.com", "mrporter.com", "theoutnet.com", "yoox.it"], + [ + "sph.com.sg", + + "sphdigital.com", + + "beritaharian.sg", + "businesstimes.com.sg", + "shinmin.sg", + "straitstimes.com", + "tabla.com.sg", + "tamilmurasu.com.sg", + "tnp.sg", + "wanbao.com.sg", + "zaobao.com", + "zaobao.com.sg", + "zaobao.sg" + ], + ["zendesk.com", "zopim.com"], + ["zhaopin.com", "zhaopin.cn"], + ["zillow.com", "zillowstatic.com", "zillowcloud.com", "zg-api.com"], + [ + "zoho.com", + + "zoho.com.au", + "zoho.eu", + + "zohositescontent.com", + "zohositescontent.com.au", + "zohositescontent.eu", + + "manageengine.com", + + "zohocdn.com", + "zohocorp.com", + "zohocreator.com", + "zohopublic.com", + "zohostatic.com", + ], + ["zonealarm.com", "zonelabs.com"], +]; + +/** + * Make a data structure for quick lookups of whether two domains are the same first party + */ +function makeDomainLookup(mdfpArray) { + let out = {}, + arrLength = mdfpArray.length; + for (let i = 0; i < arrLength; i++) { + let inner = new Set(mdfpArray[i]); + for (let domain of inner) { + out[domain] = inner; + } + } + return out; +} + +function makeIsMultiDomainFirstParty(domainLookup) { + return function (domain1, domain2) { + if (domain1 in domainLookup) { + return (domainLookup[domain1].has(domain2)); + } + return false; + }; +} + +let _domainLookup = makeDomainLookup(multiDomainFirstPartiesArray); +/** + * Check if two domains belong to the same effective first party + * @param {String} domain1 a base doamin + * @param {String} domain2 a second base doamin + * + * @return boolean true if the domains are the same first party + */ +let isMultiDomainFirstParty = makeIsMultiDomainFirstParty(_domainLookup); +/************************************** exports */ +return { + isMultiDomainFirstParty, + makeDomainLookup, + makeIsMultiDomainFirstParty, + multiDomainFirstPartiesArray, +}; +})(); //require scopes diff --git a/src/js/options.js b/src/js/options.js new file mode 100644 index 0000000..7ca7008 --- /dev/null +++ b/src/js/options.js @@ -0,0 +1,976 @@ +/* + * This file is part of Adblock Plus <http://adblockplus.org/>, + * Copyright (C) 2006-2013 Eyeo GmbH + * + * Adblock Plus 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. + * + * Adblock Plus 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 Adblock Plus. If not, see <http://www.gnu.org/licenses/>. + */ + +window.OPTIONS_INITIALIZED = false; +window.SLIDERS_DONE = false; + +const TOOLTIP_CONF = { + maxWidth: 400 +}; +const USER_DATA_EXPORT_KEYS = ["action_map", "snitch_map", "settings_map"]; + +let i18n = chrome.i18n; + +let constants = require("constants"); +let { getOriginsArray } = require("optionslib"); +let htmlUtils = require("htmlutils").htmlUtils; +let utils = require("utils"); + +let OPTIONS_DATA = {}; + +/* + * Loads options from pb storage and sets UI elements accordingly. + */ +function loadOptions() { + // Set page title to i18n version of "Privacy Badger Options" + document.title = i18n.getMessage("options_title"); + + // Add event listeners + $("#allowlist-form").on("submit", addDisabledSite); + $("#remove-disabled-site").on("click", removeDisabledSite); + $("#cloud-upload").on("click", uploadCloud); + $("#cloud-download").on("click", downloadCloud); + $('#importTrackerButton').on("click", loadFileChooser); + $('#importTrackers').on("change", importTrackerList); + $('#exportTrackers').on("click", exportUserData); + $('#resetData').on("click", resetData); + $('#removeAllData').on("click", removeAllData); + + if (OPTIONS_DATA.settings.showTrackingDomains) { + $('#tracking-domains-overlay').hide(); + } else { + $('#blockedResourcesContainer').hide(); + + $('#show-tracking-domains-checkbox').on("click", () => { + $('#tracking-domains-overlay').hide(); + $('#blockedResourcesContainer').show(); + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + showTrackingDomains: true + } + }); + }); + } + + // Set up input for searching through tracking domains. + $("#trackingDomainSearch").on("input", filterTrackingDomains); + $("#tracking-domains-type-filter").on("change", filterTrackingDomains); + $("#tracking-domains-status-filter").on("change", filterTrackingDomains); + $("#tracking-domains-show-not-yet-blocked").on("change", filterTrackingDomains); + + // Add event listeners for origins container. + $('#blockedResourcesContainer').on('change', 'input:radio', function () { + let $radio = $(this), + $clicker = $radio.parents('.clicker').first(), + origin = $clicker.data('origin'), + action = $radio.val(); + + // update domain slider row tooltip/status indicators + updateOrigin(origin, action, true); + + // persist the change + saveToggle(origin, action); + }); + $('#blockedResourcesContainer').on('click', '.userset .honeybadgerPowered', revertDomainControl); + $('#blockedResourcesContainer').on('click', '.removeOrigin', removeOrigin); + + // Display jQuery UI elements + $("#tabs").tabs({ + activate: function (event, ui) { + // update options page URL fragment identifier + // to preserve selected tab on page reload + history.replaceState(null, null, "#" + ui.newPanel.attr('id')); + } + }); + $("button").button(); + $("#add-disabled-site").button("option", "icons", {primary: "ui-icon-plus"}); + $("#remove-disabled-site").button("option", "icons", {primary: "ui-icon-minus"}); + $("#cloud-upload").button("option", "icons", {primary: "ui-icon-arrowreturnthick-1-n"}); + $("#cloud-download").button("option", "icons", {primary: "ui-icon-arrowreturnthick-1-s"}); + $(".importButton").button("option", "icons", {primary: "ui-icon-plus"}); + $("#exportTrackers").button("option", "icons", {primary: "ui-icon-extlink"}); + $("#resetData").button("option", "icons", {primary: "ui-icon-arrowrefresh-1-w"}); + $("#removeAllData").button("option", "icons", {primary: "ui-icon-closethick"}); + $("#show_counter_checkbox").on("click", updateShowCounter); + $("#show_counter_checkbox").prop("checked", OPTIONS_DATA.settings.showCounter); + $("#replace-widgets-checkbox") + .on("click", updateWidgetReplacement) + .prop("checked", OPTIONS_DATA.isWidgetReplacementEnabled); + $("#enable_dnt_checkbox").on("click", updateDNTCheckboxClicked); + $("#enable_dnt_checkbox").prop("checked", OPTIONS_DATA.settings.sendDNTSignal); + $("#check_dnt_policy_checkbox").on("click", updateCheckingDNTPolicy); + $("#check_dnt_policy_checkbox").prop("checked", OPTIONS_DATA.settings.checkForDNTPolicy).prop("disabled", !OPTIONS_DATA.settings.sendDNTSignal); + + // only show the alternateErrorPagesEnabled override if browser supports it + if (chrome.privacy && chrome.privacy.services && chrome.privacy.services.alternateErrorPagesEnabled) { + $("#privacy-settings-header").show(); + $("#disable-google-nav-error-service").show(); + $('#disable-google-nav-error-service-checkbox') + .prop("checked", OPTIONS_DATA.settings.disableGoogleNavErrorService) + .on("click", overrideAlternateErrorPagesSetting); + } + + // only show the hyperlinkAuditingEnabled override if browser supports it + if (chrome.privacy && chrome.privacy.websites && chrome.privacy.websites.hyperlinkAuditingEnabled) { + $("#privacy-settings-header").show(); + $("#disable-hyperlink-auditing").show(); + $("#disable-hyperlink-auditing-checkbox") + .prop("checked", OPTIONS_DATA.settings.disableHyperlinkAuditing) + .on("click", overrideHyperlinkAuditingSetting); + } + + if (OPTIONS_DATA.webRTCAvailable) { + $("#webRTCToggle").show(); + $("#toggle_webrtc_mode").on("click", toggleWebRTCIPProtection); + + chrome.privacy.network.webRTCIPHandlingPolicy.get({}, result => { + // auto check the option box if ip leak is already protected at diff levels, via pb or another extension + if (result.value == "default_public_interface_only" || result.value == "disable_non_proxied_udp") { + $("#toggle_webrtc_mode").prop("checked", true); + } + }); + } + + $('#local-learning-checkbox') + .prop("checked", OPTIONS_DATA.settings.learnLocally) + .on("click", (event) => { + const enabled = $(event.currentTarget).prop("checked"); + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + learnLocally: enabled + } + }, function () { + $("#learn-in-incognito-checkbox") + .prop("disabled", (enabled ? false : "disabled")) + .prop("checked", (enabled ? OPTIONS_DATA.settings.learnInIncognito : false)); + $("#show-nontracking-domains-checkbox") + .prop("disabled", (enabled ? false : "disabled")) + .prop("checked", (enabled ? OPTIONS_DATA.settings.showNonTrackingDomains : false)); + + $("#learning-setting-divs").slideToggle(enabled); + $("#not-yet-blocked-filter").toggle(enabled); + }); + }); + if (OPTIONS_DATA.settings.learnLocally) { + $("#learning-setting-divs").show(); + $("#not-yet-blocked-filter").show(); + } + + $("#learn-in-incognito-checkbox") + .prop("disabled", OPTIONS_DATA.settings.learnLocally ? false : "disabled") + .prop("checked", ( + OPTIONS_DATA.settings.learnLocally ? + OPTIONS_DATA.settings.learnInIncognito : false + )) + .on("click", (event) => { + const enabled = $(event.currentTarget).prop("checked"); + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + learnInIncognito: enabled + } + }, function () { + OPTIONS_DATA.settings.learnInIncognito = enabled; + }); + }); + + $('#show-nontracking-domains-checkbox') + .prop("disabled", OPTIONS_DATA.settings.learnLocally ? false : "disabled") + .prop("checked", ( + OPTIONS_DATA.settings.learnLocally ? + OPTIONS_DATA.settings.showNonTrackingDomains : false + )) + .on("click", (event) => { + const enabled = $(event.currentTarget).prop("checked"); + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + showNonTrackingDomains: enabled + } + }, function () { + OPTIONS_DATA.settings.showNonTrackingDomains = enabled; + }); + }); + + const widgetSelector = $("#hide-widgets-select"); + widgetSelector.prop("disabled", + OPTIONS_DATA.isWidgetReplacementEnabled ? false : "disabled"); + + $("#replace-widgets-checkbox").change(function () { + if ($(this).is(":checked")) { + widgetSelector.prop("disabled", false); + } else { + widgetSelector.prop("disabled", "disabled"); + } + }); + + // Initialize Select2 and populate options + widgetSelector.select2(); + OPTIONS_DATA.widgets.forEach(function (key) { + const isSelected = OPTIONS_DATA.settings.widgetReplacementExceptions.includes(key); + const option = new Option(key, key, false, isSelected); + widgetSelector.append(option).trigger("change"); + }); + + widgetSelector.on('select2:select', updateWidgetReplacementExceptions); + widgetSelector.on('select2:unselect', updateWidgetReplacementExceptions); + widgetSelector.on('select2:clear', updateWidgetReplacementExceptions); + + reloadDisabledSites(); + reloadTrackingDomainsTab(); + + $('html').css({ + overflow: 'visible', + visibility: 'visible' + }); + + window.OPTIONS_INITIALIZED = true; +} + +/** + * Opens the file chooser to allow a user to select + * a file to import. + */ +function loadFileChooser() { + var fileChooser = document.getElementById('importTrackers'); + fileChooser.click(); +} + +/** + * Import a list of trackers supplied by the user + * NOTE: list must be in JSON format to be parsable + */ +function importTrackerList() { + var file = this.files[0]; + + if (file) { + var reader = new FileReader(); + reader.readAsText(file); + reader.onload = function(e) { + parseUserDataFile(e.target.result); + }; + } else { + var selectFile = i18n.getMessage("import_select_file"); + confirm(selectFile); + } + + document.getElementById("importTrackers").value = ''; +} + +/** + * Parses Privacy Badger data uploaded by the user. + * + * @param {String} storageMapsList data from JSON file that user provided + */ +function parseUserDataFile(storageMapsList) { + let lists; + + try { + lists = JSON.parse(storageMapsList); + } catch (e) { + return confirm(i18n.getMessage("invalid_json")); + } + + // validate by checking we have the same keys in the import as in the export + if (!_.isEqual( + Object.keys(lists).sort(), + USER_DATA_EXPORT_KEYS.sort() + )) { + return confirm(i18n.getMessage("invalid_json")); + } + + // check for webrtc setting in the imported settings map + if (lists.settings_map.preventWebRTCIPLeak) { + // verify that the user hasn't already enabled this option + if (!$("#toggle_webrtc_mode").prop("checked")) { + toggleWebRTCIPProtection(); + } + // this browser-controlled setting doesn't belong in Badger's settings object + delete lists.settings_map.preventWebRTCIPLeak; + } + + chrome.runtime.sendMessage({ + type: "mergeUserData", + data: lists + }, (response) => { + OPTIONS_DATA.settings.disabledSites = response.disabledSites; + OPTIONS_DATA.origins = response.origins; + + reloadDisabledSites(); + reloadTrackingDomainsTab(); + // TODO general settings are not updated + + confirm(i18n.getMessage("import_successful")); + }); +} + +function resetData() { + var resetWarn = i18n.getMessage("reset_data_confirm"); + if (confirm(resetWarn)) { + chrome.runtime.sendMessage({type: "resetData"}, () => { + // reload page to refresh tracker list + location.reload(); + }); + } +} + +function removeAllData() { + var removeWarn = i18n.getMessage("remove_all_data_confirm"); + if (confirm(removeWarn)) { + chrome.runtime.sendMessage({type: "removeAllData"}, () => { + location.reload(); + }); + } +} + +function downloadCloud() { + chrome.runtime.sendMessage({type: "downloadCloud"}, + function (response) { + if (response.success) { + alert(i18n.getMessage("download_cloud_success")); + OPTIONS_DATA.settings.disabledSites = response.disabledSites; + reloadDisabledSites(); + } else { + console.error("Cloud sync error:", response.message); + if (response.message === i18n.getMessage("download_cloud_no_data")) { + alert(response.message); + } else { + alert(i18n.getMessage("download_cloud_failure")); + } + } + } + ); +} + +function uploadCloud() { + chrome.runtime.sendMessage({type: "uploadCloud"}, + function (status) { + if (status.success) { + alert(i18n.getMessage("upload_cloud_success")); + } else { + console.error("Cloud sync error:", status.message); + alert(i18n.getMessage("upload_cloud_failure")); + } + } + ); +} + +/** + * Export the user's data, including their list of trackers from + * action_map and snitch_map, along with their settings. + * List will be in JSON format that can be edited and reimported + * in another instance of Privacy Badger. + */ +function exportUserData() { + chrome.storage.local.get(USER_DATA_EXPORT_KEYS, function (maps) { + + // exports the user's prevent webrtc leak setting if it's checked + if ($("#toggle_webrtc_mode").prop("checked")) { + maps.settings_map.preventWebRTCIPLeak = true; + } + + let mapJSON = JSON.stringify(maps); + + // Append the formatted date to the exported file name + let currDate = new Date().toLocaleString(); + let escapedDate = currDate + // illegal filename charset regex from + // https://github.com/parshap/node-sanitize-filename/blob/ef1e8ad58e95eb90f8a01f209edf55cd4176e9c8/index.js + .replace(/[\/\?<>\\:\*\|"]/g, '_') /* eslint no-useless-escape:off */ + // also collapse-replace commas and spaces + .replace(/[, ]+/g, '_'); + let filename = 'PrivacyBadger_user_data-' + escapedDate + '.json'; + + // Download workaround taken from uBlock Origin + // https://github.com/gorhill/uBlock/blob/40a85f8c04840ae5f5875c1e8b5fa17578c5bd1a/platform/chromium/vapi-common.js + let a = document.createElement('a'); + a.setAttribute('download', filename || ''); + + let blob = new Blob([mapJSON], { type: 'application/json' }); // pass a useful mime type here + a.href = URL.createObjectURL(blob); + + function clickBlobLink() { + a.dispatchEvent(new MouseEvent('click')); + URL.revokeObjectURL(blob); + } + + /** + * Firefox workaround to insert the blob link in an iFrame + * https://bugzilla.mozilla.org/show_bug.cgi?id=1420419#c18 + */ + function addBlobWorkAroundForFirefox() { + // Create or use existing iframe for the blob 'a' element + let iframe = document.getElementById('exportUserDataIframe'); + if (!iframe) { + iframe = document.createElement('iframe'); + iframe.id = "exportUserDataIframe"; + iframe.setAttribute("style", "visibility: hidden; height: 0; width: 0"); + document.getElementById('export').appendChild(iframe); + + iframe.contentWindow.document.open(); + iframe.contentWindow.document.write('<html><head></head><body></body></html>'); + iframe.contentWindow.document.close(); + } else { + // Remove the old 'a' element from the iframe + let oldElement = iframe.contentWindow.document.body.lastChild; + iframe.contentWindow.document.body.removeChild(oldElement); + } + iframe.contentWindow.document.body.appendChild(a); + } + + // TODO remove browser check and simplify code once Firefox 58 goes away + // https://bugzilla.mozilla.org/show_bug.cgi?id=1420419 + if (chrome.runtime.getBrowserInfo) { + chrome.runtime.getBrowserInfo((info) => { + if (info.name == "Firefox" || info.name == "Waterfox") { + addBlobWorkAroundForFirefox(); + } + clickBlobLink(); + }); + } else { + clickBlobLink(); + } + }); +} + +/** + * Update setting for whether or not to show counter on Privacy Badger badge. + */ +function updateShowCounter() { + const showCounter = $("#show_counter_checkbox").prop("checked"); + + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { showCounter } + }, () => { + // Refresh display for each tab's PB badge. + chrome.tabs.query({}, function(tabs) { + tabs.forEach(function(tab) { + chrome.runtime.sendMessage({ + type: "updateBadge", + tab_id: tab.id + }); + }); + }); + }); +} + +/** + * Update setting for whether or not to replace + * social buttons/video players/commenting widgets. + */ +function updateWidgetReplacement() { + const socialWidgetReplacementEnabled = $("#replace-widgets-checkbox").prop("checked"); + + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { socialWidgetReplacementEnabled } + }); +} + +/** + * Update DNT checkbox clicked + */ +function updateDNTCheckboxClicked() { + const enabled = $("#enable_dnt_checkbox").prop("checked"); + + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + sendDNTSignal: enabled + } + }); + + $("#check_dnt_policy_checkbox").prop("checked", enabled).prop("disabled", !enabled); + updateCheckingDNTPolicy(); +} + +function updateCheckingDNTPolicy() { + const enabled = $("#check_dnt_policy_checkbox").prop("checked"); + + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + checkForDNTPolicy: enabled + } + }); +} + +function reloadDisabledSites() { + let sites = OPTIONS_DATA.settings.disabledSites, + $select = $('#allowlist-select'); + + // sort disabled sites the same way blocked sites are sorted + sites = htmlUtils.sortDomains(sites); + + $select.empty(); + for (let i = 0; i < sites.length; i++) { + $('<option>').text(sites[i]).appendTo($select); + } +} + +function addDisabledSite(event) { + event.preventDefault(); + + let domain = utils.getHostFromDomainInput( + document.getElementById("new-disabled-site-input").value.replace(/\s/g, "") + ); + + if (!domain) { + return confirm(i18n.getMessage("invalid_domain")); + } + + chrome.runtime.sendMessage({ + type: "disablePrivacyBadgerForOrigin", + domain + }, (response) => { + OPTIONS_DATA.settings.disabledSites = response.disabledSites; + reloadDisabledSites(); + document.getElementById("new-disabled-site-input").value = ""; + }); +} + +function removeDisabledSite(event) { + event.preventDefault(); + + let domains = []; + let $selected = $("#allowlist-select option:selected"); + for (let i = 0; i < $selected.length; i++) { + domains.push($selected[i].text); + } + + chrome.runtime.sendMessage({ + type: "enablePrivacyBadgerForOriginList", + domains + }, (response) => { + OPTIONS_DATA.settings.disabledSites = response.disabledSites; + reloadDisabledSites(); + }); +} + +// Tracking Domains slider functions + +/** + * Gets action for given origin. + * @param {String} origin - Origin to get action for. + */ +function getOriginAction(origin) { + return OPTIONS_DATA.origins[origin]; +} + +function revertDomainControl(event) { + event.preventDefault(); + + let origin = $(event.target).parent().data('origin'); + + chrome.runtime.sendMessage({ + type: "revertDomainControl", + origin + }, (response) => { + // update any sliders that changed as a result + updateSliders(response.origins); + // update cached domain data + OPTIONS_DATA.origins = response.origins; + }); +} + +/** + * Displays list of all tracking domains along with toggle controls. + */ +function updateSummary() { + // if there are no tracking domains + let allTrackingDomains = Object.keys(OPTIONS_DATA.origins); + if (!allTrackingDomains || !allTrackingDomains.length) { + // hide the number of trackers and slider instructions message + $("#options_domain_list_trackers").hide(); + + // show "no trackers" message + $("#options_domain_list_no_trackers").show(); + $("#blockedResources").html(''); + $("#tracking-domains-div").hide(); + + // activate tooltips + $('.tooltip:not(.tooltipstered)').tooltipster(TOOLTIP_CONF); + + return; + } + + // reloadTrackingDomainsTab can be called multiple times, needs to be reversible + $("#options_domain_list_no_trackers").hide(); + $("#tracking-domains-div").show(); + + // count unique (cookie)blocked tracking base domains + let blockedDomains = getOriginsArray(OPTIONS_DATA.origins, null, null, null, false); + let baseDomains = new Set(blockedDomains.map(d => window.getBaseDomain(d))); + $("#options_domain_list_trackers").html(i18n.getMessage( + "options_domain_list_trackers", [ + baseDomains.size, + "<a target='_blank' title='" + _.escape(i18n.getMessage("what_is_a_tracker")) + "' class='tooltip' href='https://privacybadger.org/#What-is-a-third-party-tracker'>" + ] + )).show(); +} + +/** + * Displays list of all tracking domains along with toggle controls. + */ +function reloadTrackingDomainsTab() { + updateSummary(); + + // Get containing HTML for domain list along with toggle legend icons. + $("#blockedResources")[0].innerHTML = htmlUtils.getTrackerContainerHtml(); + + // activate tooltips + $('.tooltip:not(.tooltipstered)').tooltipster(TOOLTIP_CONF); + + // Display tracking domains. + showTrackingDomains( + getOriginsArray( + OPTIONS_DATA.origins, + $("#trackingDomainSearch").val(), + $('#tracking-domains-type-filter').val(), + $('#tracking-domains-status-filter').val(), + $('#tracking-domains-show-not-yet-blocked').prop('checked') + ) + ); +} + +/** + * Displays filtered list of tracking domains based on user input. + */ +function filterTrackingDomains() { + const $searchFilter = $('#trackingDomainSearch'), + $typeFilter = $('#tracking-domains-type-filter'), + $statusFilter = $('#tracking-domains-status-filter'); + + if ($typeFilter.val() == "dnt") { + $statusFilter.prop("disabled", true).val(""); + } else { + $statusFilter.prop("disabled", false); + } + + let search_update = (this == $searchFilter[0]), + initial_search_text = $searchFilter.val().toLowerCase(), + time_to_wait = 0, + callback = function () {}; + + // If we are here because the search filter got updated, + // wait a short period of time and see if search text has changed. + // If so it means user is still typing so hold off on filtering. + if (search_update) { + time_to_wait = 500; + callback = function () { + $searchFilter.focus(); + }; + } + + setTimeout(function () { + // check search text + let search_text = $searchFilter.val().toLowerCase(); + if (search_text != initial_search_text) { + return; + } + + // show filtered origins + let filteredOrigins = getOriginsArray( + OPTIONS_DATA.origins, + search_text, + $typeFilter.val(), + $statusFilter.val(), + $('#tracking-domains-show-not-yet-blocked').prop('checked') + ); + showTrackingDomains(filteredOrigins, callback); + + }, time_to_wait); +} + +/** + * Renders the list of tracking domains. + * + * @param {Array} domains + * @param {Function} cb callback + */ +function showTrackingDomains(domains, cb) { + if (!cb) { + cb = function () {}; + } + + window.SLIDERS_DONE = false; + $('#tracking-domains-div').css('visibility', 'hidden'); + $('#tracking-domains-loader').show(); + + domains = htmlUtils.sortDomains(domains); + + let out = []; + for (let domain of domains) { + let action = getOriginAction(domain); + if (action) { + let show_breakage_warning = ( + action == constants.USER_BLOCK && + OPTIONS_DATA.cookieblocked.hasOwnProperty(domain) + ); + out.push(htmlUtils.getOriginHtml(domain, action, show_breakage_warning)); + } + } + + function renderDomains() { + const CHUNK = 100; + + let $printable = $(out.splice(0, CHUNK).join("")); + + $printable.appendTo('#blockedResourcesInner'); + + // activate tooltips + // TODO disabled for performance reasons + //$('#blockedResourcesInner .tooltip:not(.tooltipstered)').tooltipster( + // htmlUtils.DOMAIN_TOOLTIP_CONF); + + if (out.length) { + requestAnimationFrame(renderDomains); + } else { + $('#tracking-domains-loader').hide(); + $('#tracking-domains-div').css('visibility', 'visible'); + window.SLIDERS_DONE = true; + cb(); + } + } + + $('#blockedResourcesInner').empty(); + + if (out.length) { + requestAnimationFrame(renderDomains); + } else { + $('#tracking-domains-loader').hide(); + $('#tracking-domains-div').css('visibility', 'visible'); + window.SLIDERS_DONE = true; + cb(); + } +} + +/** + * https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-01#page-5 + * + * Toggle WebRTC IP address leak protection setting. + * + * When enabled, policy is set to Mode 3 (default_public_interface_only). + */ +function toggleWebRTCIPProtection() { + // Return early with non-supporting browsers + if (!OPTIONS_DATA.webRTCAvailable) { + return; + } + + let cpn = chrome.privacy.network; + + cpn.webRTCIPHandlingPolicy.get({}, function (result) { + // Update new value to be opposite of current browser setting + if (result.value == 'default_public_interface_only') { + cpn.webRTCIPHandlingPolicy.clear({}); + } else { + cpn.webRTCIPHandlingPolicy.set({ + value: 'default_public_interface_only' + }); + } + }); +} + +// handles overriding the alternateErrorPagesEnabled setting +function overrideAlternateErrorPagesSetting() { + const checked = $("#disable-google-nav-error-service-checkbox").prop("checked"); + + // update Badger settings so that we know to reapply the browser setting on startup + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + disableGoogleNavErrorService: checked + } + }); + + // update the browser setting + if (checked) { + chrome.privacy.services.alternateErrorPagesEnabled.set({ + value: false + }); + } else { + chrome.privacy.services.alternateErrorPagesEnabled.clear({}); + } +} + +// handles overriding the hyperlinkAuditingEnabled setting +function overrideHyperlinkAuditingSetting() { + const checked = $("#disable-hyperlink-auditing-checkbox").prop("checked"); + + // update Badger settings so that we know to reapply the browser setting on startup + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { + disableHyperlinkAuditing: checked + } + }); + + // update the browser setting + if (checked) { + chrome.privacy.websites.hyperlinkAuditingEnabled.set({ + value: false + }); + } else { + chrome.privacy.websites.hyperlinkAuditingEnabled.clear({}); + } +} + +/** + * Updates domain tooltip, slider color. + * Also toggles status indicators like breakage warnings. + */ +function updateOrigin(origin, action, userset) { + let $clicker = $('#blockedResourcesInner div.clicker[data-origin="' + origin + '"]'), + $switchContainer = $clicker.find('.switch-container').first(); + + // update slider color via CSS + $switchContainer.removeClass([ + constants.BLOCK, + constants.COOKIEBLOCK, + constants.ALLOW, + constants.NO_TRACKING].join(" ")).addClass(action); + + let show_breakage_warning = ( + action == constants.BLOCK && + OPTIONS_DATA.cookieblocked.hasOwnProperty(origin) + ); + + htmlUtils.toggleBlockedStatus($clicker, userset, show_breakage_warning); + + // reinitialize the domain tooltip + // TODO disabled for performance reasons + //$clicker.find('.origin-inner').tooltipster('destroy'); + //$clicker.find('.origin-inner').attr( + // 'title', htmlUtils.getActionDescription(action, origin)); + //$clicker.find('.origin-inner').tooltipster(htmlUtils.DOMAIN_TOOLTIP_CONF); +} + +/** + * Updates the list of tracking domains in response to user actions. + * + * For example, moving the slider for example.com should move the sliders + * for www.example.com and cdn.example.com + */ +function updateSliders(updatedOriginData) { + let updated_domains = Object.keys(updatedOriginData); + + // update any sliders that changed + for (let domain of updated_domains) { + let action = updatedOriginData[domain]; + if (action == OPTIONS_DATA.origins[domain]) { + continue; + } + + let userset = false; + if (action.startsWith('user')) { + userset = true; + action = action.slice(5); + } + + // update slider position + let $radios = $('#blockedResourcesInner div.clicker[data-origin="' + domain + '"] input'), + selected_val = (action == constants.DNT ? constants.ALLOW : action); + // update the radio group without triggering a change event + // https://stackoverflow.com/a/22635728 + $radios.val([selected_val]); + + // update domain slider row tooltip/status indicators + updateOrigin(domain, action, userset); + } + + // remove sliders that are no longer present + let removed = Object.keys(OPTIONS_DATA.origins).filter( + x => !updated_domains.includes(x)); + for (let domain of removed) { + let $clicker = $('#blockedResourcesInner div.clicker[data-origin="' + domain + '"]'); + $clicker.remove(); + } +} + +/** + * Save the user setting for a domain by messaging the background page. + */ +function saveToggle(origin, action) { + chrome.runtime.sendMessage({ + type: "saveOptionsToggle", + origin, + action + }, (response) => { + // first update the cache for the slider + // that was just changed by the user + // to avoid redundantly updating it below + OPTIONS_DATA.origins[origin] = response.origins[origin]; + // update any sliders that changed as a result + updateSliders(response.origins); + // update cached domain data + OPTIONS_DATA.origins = response.origins; + }); +} + +/** + * Remove origin from Privacy Badger. + * @param {Event} event Click event triggered by user. + */ +function removeOrigin(event) { + event.preventDefault(); + + // confirm removal before proceeding + if (!confirm(i18n.getMessage("options_remove_origin_confirm"))) { + return; + } + + let origin = $(event.target).parent().data('origin'); + + chrome.runtime.sendMessage({ + type: "removeOrigin", + origin + }, (response) => { + // remove rows that are no longer here + updateSliders(response.origins); + // update cached domain data + OPTIONS_DATA.origins = response.origins; + // if we removed domains, the summary text may have changed + updateSummary(); + }); +} + +/** + * Update which widgets should be blocked instead of replaced + * @param {Event} event The DOM event triggered by selecting an option + */ +function updateWidgetReplacementExceptions() { + const widgetReplacementExceptions = $('#hide-widgets-select').select2('data').map(({ id }) => id); + chrome.runtime.sendMessage({ + type: "updateSettings", + data: { widgetReplacementExceptions } + }); +} + +$(function () { + $.tooltipster.setDefaults(htmlUtils.TOOLTIPSTER_DEFAULTS); + + chrome.runtime.sendMessage({ + type: "getOptionsData", + }, (response) => { + OPTIONS_DATA = response; + loadOptions(); + }); +}); diff --git a/src/js/popup.js b/src/js/popup.js new file mode 100644 index 0000000..7e02e23 --- /dev/null +++ b/src/js/popup.js @@ -0,0 +1,723 @@ +/* + * 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/>. + */ + +window.POPUP_INITIALIZED = false; +window.SLIDERS_DONE = false; + +var constants = require("constants"); +var FirefoxAndroid = require("firefoxandroid"); +var htmlUtils = require("htmlutils").htmlUtils; + +let POPUP_DATA = {}; + +/* if they aint seen the comic*/ +function showNagMaybe() { + var $nag = $("#instruction"); + var $outer = $("#instruction-outer"); + let intro_page_url = chrome.runtime.getURL("/skin/firstRun.html"); + + function _setSeenComic(cb) { + chrome.runtime.sendMessage({ + type: "seenComic" + }, cb); + } + + function _setSeenLearningPrompt(cb) { + chrome.runtime.sendMessage({ + type: "seenLearningPrompt" + }, cb); + } + + function _hideNag() { + $nag.fadeOut(); + $outer.fadeOut(); + } + + function _showNag() { + $nag.show(); + $outer.show(); + // Attach event listeners + $('#fittslaw').on("click", function (e) { + e.preventDefault(); + _setSeenComic(() => { + _hideNag(); + }); + }); + $("#intro-reminder-btn").on("click", function () { + // If there is a firstRun.html tab, switch to the tab. + // Otherwise, create a new tab + chrome.tabs.query({url: intro_page_url}, function (tabs) { + if (tabs.length == 0) { + chrome.tabs.create({ + url: intro_page_url + }); + } else { + chrome.tabs.update(tabs[0].id, {active: true}, function (tab) { + chrome.windows.update(tab.windowId, {focused: true}); + }); + } + _setSeenComic(() => { + window.close(); + }); + }); + }); + } + + function _showError(error_text) { + $('#instruction-text').hide(); + $('#error-text').show().find('a') + .attr('id', 'critical-error-link') + .css({ + padding: '5px', + display: 'inline-block', + width: 'auto', + }); + $('#error-message').text(error_text); + + $('#fittslaw').on("click", function (e) { + e.preventDefault(); + _hideNag(); + }); + + $nag.show(); + $outer.show(); + } + + function _showLearningPrompt() { + $('#instruction-text').hide(); + + $("#learning-prompt-btn").on("click", function () { + chrome.tabs.create({ + url: "https://www.eff.org/badger-evolution" + }); + _setSeenLearningPrompt(function () { + window.close(); + }); + }); + + $('#fittslaw').on("click", function (e) { + e.preventDefault(); + _setSeenLearningPrompt(function () { + _hideNag(); + }); + }); + + $('#learning-prompt-div').show(); + $nag.show(); + $outer.show(); + } + + if (POPUP_DATA.showLearningPrompt) { + _showLearningPrompt(); + + } else if (!POPUP_DATA.seenComic) { + chrome.tabs.query({active: true, currentWindow: true}, function (focusedTab) { + // Show the popup instruction if the active tab is not firstRun.html page + if (!focusedTab[0].url.startsWith(intro_page_url)) { + _showNag(); + } + }); + + } else if (POPUP_DATA.criticalError) { + _showError(POPUP_DATA.criticalError); + } +} + +/** + * Init function. Showing/hiding popup.html elements and setting up event handler + */ +function init() { + showNagMaybe(); + + $("#activate_site_btn").on("click", activateOnSite); + $("#deactivate_site_btn").on("click", deactivateOnSite); + $("#donate").on("click", function() { + chrome.tabs.create({ + url: "https://supporters.eff.org/donate/support-privacy-badger" + }); + }); + + $('#error_input').on('input propertychange', function() { + // No easy way of sending message on popup close, send message for every change + chrome.runtime.sendMessage({ + type: 'saveErrorText', + tabId: POPUP_DATA.tabId, + errorText: $("#error_input").val() + }); + }); + + let overlay = $('#overlay'); + + // show error layout if the user was writing an error report + if (POPUP_DATA.hasOwnProperty('errorText') && POPUP_DATA.errorText) { + overlay.toggleClass('active'); + } + + $("#error").on("click", function() { + overlay.toggleClass('active'); + }); + $("#report-cancel").on("click", function() { + clearSavedErrorText(); + closeOverlay(); + }); + $("#report-button").on("click", function() { + $(this).prop("disabled", true); + $("#report-cancel").prop("disabled", true); + send_error($("#error_input").val()); + }); + $("#report_close").on("click", function (e) { + e.preventDefault(); + clearSavedErrorText(); + closeOverlay(); + }); + $('#blockedResourcesContainer').on('change', 'input:radio', updateOrigin); + $('#blockedResourcesContainer').on('click', '.userset .honeybadgerPowered', revertDomainControl); + + $("#version").text( + chrome.i18n.getMessage("version", chrome.runtime.getManifest().version) + ); + + // improve on Firefox's built-in options opening logic + if (typeof browser == "object" && typeof browser.runtime.getBrowserInfo == "function") { + browser.runtime.getBrowserInfo().then(function (info) { + if (info.name == "Firefox") { + $("#options").on("click", function (e) { + e.preventDefault(); + openPage(chrome.runtime.getURL("/skin/options.html")); + }); + $("#help").on("click", function (e) { + e.preventDefault(); + openPage(this.getAttribute('href')); + }); + } + }); + } + + $("#share").on("click", function (e) { + e.preventDefault(); + share(); + }); + $("#share_close").on("click", function (e) { + e.preventDefault(); + $("#share_overlay").toggleClass('active', false); + }); + $("#copy-button").on("click", function() { + $("#share_output").select(); + document.execCommand('copy'); + $(this).text(chrome.i18n.getMessage("copy_button_copied")); + }); + + window.POPUP_INITIALIZED = true; +} + +function openPage(url) { + // first get the active tab + chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { + let activeTab = tabs[0], + tabProps = { + url, + windowId: activeTab.windowId, + active: true, + index: activeTab.index + 1, + openerTabId: activeTab.id + }; + + // create the new tab + try { + chrome.tabs.create(tabProps); + } catch (e) { + // TODO workaround for pre-57 Firefox + delete tabProps.openerTabId; + chrome.tabs.create(tabProps); + } + + window.close(); + }); +} + +function clearSavedErrorText() { + chrome.runtime.sendMessage({ + type: 'removeErrorText', + tabId: POPUP_DATA.tabId + }); +} + +/** + * Close the error reporting overlay + */ +function closeOverlay() { + $('#overlay').toggleClass('active', false); + $("#report-success").hide(); + $("#report-fail").hide(); + $("#error_input").val(""); +} + +/** + * Send errors to PB error reporting server + * + * @param {String} message The message to send + */ +function send_error(message) { + // get the latest domain list from the background page + chrome.runtime.sendMessage({ + type: "getPopupData", + tabId: POPUP_DATA.tabId, + tabUrl: POPUP_DATA.tabUrl + }, (response) => { + const origins = response.origins; + + if (!origins) { + return; + } + + let out = { + browser: window.navigator.userAgent, + fqdn: response.tabHost, + message: message, + url: response.tabUrl, + version: chrome.runtime.getManifest().version + }; + + for (let origin in origins) { + let action = origins[origin]; + + // adjust action names for error reporting + if (action == constants.USER_ALLOW) { + action = "usernoaction"; + } else if (action == constants.USER_BLOCK) { + action = "userblock"; + } else if (action == constants.USER_COOKIEBLOCK) { + action = "usercookieblock"; + } else if (action == constants.ALLOW) { + action = "noaction"; + } else if (action == constants.BLOCK || action == constants.COOKIEBLOCK) { + // no need to adjust action + } else if (action == constants.DNT || action == constants.NO_TRACKING) { + action = "notracking"; + } + + if (out[action]) { + out[action] += ","+origin; + } else { + out[action] = origin; + } + } + + var sendReport = $.ajax({ + type: "POST", + url: "https://privacybadger.org/reporting", + data: JSON.stringify(out), + contentType: "application/json" + }); + + sendReport.done(function() { + $("#error_input").val(""); + $("#report-success").slideDown(); + + clearSavedErrorText(); + + setTimeout(function() { + $("#report-button").prop("disabled", false); + $("#report-cancel").prop("disabled", false); + closeOverlay(); + }, 3000); + }); + + sendReport.fail(function() { + $("#report-fail").slideDown(); + + setTimeout(function() { + $("#report-button").prop("disabled", false); + $("#report-cancel").prop("disabled", false); + $("#report-fail").slideUp(); + }, 3000); + }); + }); +} + +/** + * activate PB for site event handler + */ +function activateOnSite() { + $("#activate_site_btn").toggle(); + $("#deactivate_site_btn").toggle(); + $("#blockedResourcesContainer").show(); + + chrome.runtime.sendMessage({ + type: "activateOnSite", + tabHost: POPUP_DATA.tabHost, + tabId: POPUP_DATA.tabId, + tabUrl: POPUP_DATA.tabUrl + }, () => { + chrome.tabs.reload(POPUP_DATA.tabId); + window.close(); + }); +} + +/** + * de-activate PB for site event handler + */ +function deactivateOnSite() { + $("#activate_site_btn").toggle(); + $("#deactivate_site_btn").toggle(); + $("#blockedResourcesContainer").hide(); + + chrome.runtime.sendMessage({ + type: "deactivateOnSite", + tabHost: POPUP_DATA.tabHost, + tabId: POPUP_DATA.tabId, + tabUrl: POPUP_DATA.tabUrl + }, () => { + chrome.tabs.reload(POPUP_DATA.tabId); + window.close(); + }); +} + +/** + * Open the share overlay + */ +function share() { + $("#share_overlay").toggleClass('active'); + let share_msg = chrome.i18n.getMessage("share_base_message"); + + // only add language about found trackers if we actually found trackers + // (but regardless of whether we are actually blocking them) + if (POPUP_DATA.noTabData) { + $("#share_output").val(share_msg); + return; + } + + let origins = POPUP_DATA.origins; + let originsArr = []; + if (origins) { + originsArr = Object.keys(origins); + } + + if (!originsArr.length) { + $("#share_output").val(share_msg); + return; + } + + originsArr = htmlUtils.sortDomains(originsArr); + let tracking = []; + + for (let origin of originsArr) { + let action = origins[origin]; + + if (action == constants.BLOCK || action == constants.COOKIEBLOCK) { + tracking.push(origin); + } + } + + if (tracking.length) { + share_msg += "\n\n"; + share_msg += chrome.i18n.getMessage( + "share_tracker_header", [tracking.length, POPUP_DATA.tabHost]); + share_msg += "\n\n"; + share_msg += tracking.join("\n"); + } + $("#share_output").val(share_msg); +} + +/** + * Handler to undo user selection for a tracker + */ +function revertDomainControl(event) { + event.preventDefault(); + + let origin = $(event.target).parent().data('origin'); + + chrome.runtime.sendMessage({ + type: "revertDomainControl", + origin: origin + }, () => { + chrome.tabs.reload(POPUP_DATA.tabId); + window.close(); + }); +} + +/** + * Refresh the content of the popup window + * + * @param {Integer} tabId The id of the tab + */ +function refreshPopup() { + window.SLIDERS_DONE = false; + + // must be a special browser page, + if (POPUP_DATA.noTabData) { + // show the "nothing to do here" message + $('#blockedResourcesContainer').hide(); + $('#special-browser-page').show(); + + // hide inapplicable buttons + $('#deactivate_site_btn').hide(); + $('#error').hide(); + + // activate tooltips + $('.tooltip').tooltipster(); + + window.SLIDERS_DONE = true; + + return; + } + + // revert any hiding/showing above for cases when refreshPopup gets called + // more than once for the same popup, such as during functional testing + $('#blockedResourcesContainer').show(); + $('#special-browser-page').hide(); + $('#deactivate_site_btn').show(); + $('#error').show(); + + // toggle activation buttons if privacy badger is not enabled for current url + if (!POPUP_DATA.enabled) { + $("#blockedResourcesContainer").hide(); + $("#activate_site_btn").show(); + $("#deactivate_site_btn").hide(); + $("#disabled-site-message").show(); + $("#title").addClass("faded-bw-color-scheme"); + } + + // if there is any saved error text, fill the error input with it + if (POPUP_DATA.hasOwnProperty('errorText')) { + $("#error_input").val(POPUP_DATA.errorText); + } + + let origins = POPUP_DATA.origins; + let originsArr = []; + if (origins) { + originsArr = Object.keys(origins); + } + + if (!originsArr.length) { + // hide the number of trackers and slider instructions message + // if no sliders will be displayed + $("#instructions-many-trackers").hide(); + + // show "no trackers" message + $("#instructions-no-trackers").show(); + + if (POPUP_DATA.learnLocally && POPUP_DATA.showNonTrackingDomains) { + // show the "no third party resources on this site" message + $("#no-third-parties").show(); + } + + // activate tooltips + $('.tooltip').tooltipster(); + + window.SLIDERS_DONE = true; + + return; + } + + let printable = []; + let unblockedTrackers = []; + let nonTracking = []; + originsArr = htmlUtils.sortDomains(originsArr); + + for (let origin of originsArr) { + let action = origins[origin]; + + if (action == constants.NO_TRACKING) { + nonTracking.push(origin); + } else if (action == constants.ALLOW) { + unblockedTrackers.push(origin); + } else { + let show_breakage_warning = ( + action == constants.USER_BLOCK && + POPUP_DATA.cookieblocked.hasOwnProperty(origin) + ); + printable.push( + htmlUtils.getOriginHtml(origin, action, show_breakage_warning) + ); + } + } + + if (POPUP_DATA.learnLocally && unblockedTrackers.length) { + printable.push( + '<div class="clicker tooltip" id="not-yet-blocked-header" title="' + + chrome.i18n.getMessage("intro_not_an_adblocker_paragraph") + + '" data-tooltipster=\'{"side":"top"}\'>' + + chrome.i18n.getMessage("not_yet_blocked_header") + + '</div>' + ); + unblockedTrackers.forEach(domain => { + printable.push( + htmlUtils.getOriginHtml(domain, constants.ALLOW) + ); + }); + + // reduce margin if we have hasn't-decided-yet-to-block domains to show + $("#instructions-no-trackers").css("margin", "10px 0"); + } + + if (POPUP_DATA.learnLocally && POPUP_DATA.showNonTrackingDomains && nonTracking.length) { + printable.push( + '<div class="clicker tooltip" id="non-trackers-header" title="' + + chrome.i18n.getMessage("non_tracker_tip") + + '" data-tooltipster=\'{"side":"top"}\'>' + + chrome.i18n.getMessage("non_tracker") + + '</div>' + ); + for (let i = 0; i < nonTracking.length; i++) { + printable.push( + htmlUtils.getOriginHtml(nonTracking[i], constants.NO_TRACKING) + ); + } + + // reduce margin if we have non-tracking domains to show + $("#instructions-no-trackers").css("margin", "10px 0"); + } + + if (printable.length) { + // get containing HTML for domain list along with toggle legend icons + $("#blockedResources")[0].innerHTML = htmlUtils.getTrackerContainerHtml(); + } + + // activate tooltips + $('.tooltip').tooltipster(); + + if (POPUP_DATA.trackerCount === 0) { + // hide multiple trackers message + $("#instructions-many-trackers").hide(); + + // show "no trackers" message + $("#instructions-no-trackers").show(); + + } else { + $('#instructions-many-trackers').html(chrome.i18n.getMessage( + "popup_instructions", [ + POPUP_DATA.trackerCount, + "<a target='_blank' title='" + _.escape(chrome.i18n.getMessage("what_is_a_tracker")) + "' class='tooltip' href='https://privacybadger.org/#What-is-a-third-party-tracker'>" + ] + )).find(".tooltip").tooltipster(); + } + + function renderDomains() { + const CHUNK = 1; + + let $printable = $(printable.splice(0, CHUNK).join("")); + + // Hide elements for removing origins (controlled from the options page). + // Popup shows what's loaded for the current page so it doesn't make sense + // to have removal ability here. + $printable.find('.removeOrigin').hide(); + + $printable.appendTo('#blockedResourcesInner'); + + // activate tooltips + $('#blockedResourcesInner .tooltip:not(.tooltipstered)').tooltipster( + htmlUtils.DOMAIN_TOOLTIP_CONF); + + if (printable.length) { + requestAnimationFrame(renderDomains); + } else { + window.SLIDERS_DONE = true; + } + } + + if (printable.length) { + requestAnimationFrame(renderDomains); + } else { + window.SLIDERS_DONE = true; + } +} + +/** + * Update the user preferences displayed in the domain list for this origin. + * These UI changes will later be used to update user preferences data. + * + * @param {Event} event Click event triggered by user. + */ +function updateOrigin() { + // get the origin and new action for it + let $radio = $(this), + action = $radio.val(), + $switchContainer = $radio.parents('.switch-container').first(); + + // update slider color via CSS + $switchContainer.removeClass([ + constants.BLOCK, + constants.COOKIEBLOCK, + constants.ALLOW, + constants.NO_TRACKING].join(" ")).addClass(action); + + let $clicker = $radio.parents('.clicker').first(), + origin = $clicker.data('origin'), + show_breakage_warning = ( + action == constants.BLOCK && + POPUP_DATA.cookieblocked.hasOwnProperty(origin) + ); + + htmlUtils.toggleBlockedStatus($clicker, true, show_breakage_warning); + + // reinitialize the domain tooltip + $clicker.find('.origin-inner').tooltipster('destroy'); + $clicker.find('.origin-inner').attr( + 'title', htmlUtils.getActionDescription(action, origin)); + $clicker.find('.origin-inner').tooltipster(htmlUtils.DOMAIN_TOOLTIP_CONF); + + // persist the change + saveToggle(origin, action); +} + +/** + * Save the user setting for a domain by messaging the background page. + */ +function saveToggle(origin, action) { + chrome.runtime.sendMessage({ + type: "savePopupToggle", + origin, + action, + tabId: POPUP_DATA.tabId + }); +} + +function getTab(callback) { + // Temporary fix for Firefox Android + if (!FirefoxAndroid.hasPopupSupport) { + FirefoxAndroid.getParentOfPopup(callback); + return; + } + + chrome.tabs.query({active: true, lastFocusedWindow: true}, function(t) { callback(t[0]); }); +} + +/** + * Workaround for geckodriver being unable to modify page globals. + */ +function setPopupData(data) { + POPUP_DATA = data; +} + +$(function () { + $.tooltipster.setDefaults(htmlUtils.TOOLTIPSTER_DEFAULTS); + + getTab(function (tab) { + chrome.runtime.sendMessage({ + type: "getPopupData", + tabId: tab.id, + tabUrl: tab.url + }, (response) => { + setPopupData(response); + refreshPopup(); + init(); + }); + }); +}); diff --git a/src/js/socialwidgetloader.js b/src/js/socialwidgetloader.js new file mode 100644 index 0000000..11f9bce --- /dev/null +++ b/src/js/socialwidgetloader.js @@ -0,0 +1,133 @@ +/* + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2014 Electronic Frontier Foundation + * Derived from ShareMeNot + * Copyright (C) 2011-2014 University of Washington + * + * 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/>. + */ + +/* + * ShareMeNot is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright (c) 2011-2014 University of Washington + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* globals log:false */ + +require.scopes.widgetloader = (function () { + +let utils = require('utils'); + +let exports = { + initializeWidgets, + loadWidgetsFromFile, +}; + +/** + * Returns the contents of the file at filePath. + * + * @param {String} filePath the path to the file + * @param {Function} callback callback(responseText) + */ +function getFileContents(filePath, callback) { + let url = chrome.runtime.getURL(filePath); + utils.xhrRequest(url, function (err, responseText) { + if (err) { + console.error( + "Problem fetching contents of file at", + filePath, + err.status, + err.message + ); + } else { + callback(responseText); + } + }); +} + +/** + * @param {String} file_path the path to the JSON file + * @returns {Promise} resolved with an array of SocialWidget objects + */ +function loadWidgetsFromFile(file_path) { + return new Promise(function (resolve) { + getFileContents(file_path, function (contents) { + let widgets = initializeWidgets(JSON.parse(contents)); + log("Initialized widgets from disk"); + resolve(widgets); + }); + }); +} + +/** + * @param {Object} widgetsJson widget data + * @returns {Array} array of SocialWidget objects + */ +function initializeWidgets(widgetsJson) { + let widgets = []; + + // loop over each widget, making a SocialWidget object + for (let widget_name in widgetsJson) { + let widgetProperties = widgetsJson[widget_name]; + let widget = new SocialWidget(widget_name, widgetProperties); + widgets.push(widget); + } + + return widgets; +} + +/** + * Constructs a SocialWidget with the given name and properties. + * + * @param {String} name the name of the socialwidget + * @param {Object} properties the properties of the socialwidget + */ +function SocialWidget(name, properties) { + let self = this; + + self.name = name; + + for (let property in properties) { + self[property] = properties[property]; + } + + // standardize on "domains" + if (self.domain) { + self.domains = [self.domain]; + } +} + +return exports; + +}()); //require scopes 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 */ +}()); diff --git a/src/js/surrogates.js b/src/js/surrogates.js new file mode 100644 index 0000000..5389afd --- /dev/null +++ b/src/js/surrogates.js @@ -0,0 +1,87 @@ +/* + * + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2016 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/>. + */ + +require.scopes.surrogates = (function() { + +const db = require('surrogatedb'); + +/** + * Blocking tracking scripts (trackers) can cause parts of webpages to break. + * Surrogate scripts are dummy pieces of JavaScript meant to supply just enough + * of the original tracker's functionality to allow pages to continue working. + * + * This method gets called within request-blocking listeners: + * It needs to be fast! + * + * @param {String} script_url The full URL of the script resource being requested. + * + * @param {String} script_hostname The hostname component of the script_url + * parameter. This is an optimization: the calling context should already have + * this information. + * + * @return {(String|Boolean)} The surrogate script as a data URI when there is a + * match, or boolean false when there is no match. + */ +function getSurrogateURI(script_url, script_hostname) { + // do we have an entry for the script hostname? + if (db.hostnames.hasOwnProperty(script_hostname)) { + const tokens = db.hostnames[script_hostname]; + + // it's a wildcard token + if (_.isString(tokens)) { + if (db.surrogates.hasOwnProperty(tokens)) { + // return the surrogate code + return 'data:application/javascript;base64,' + btoa(db.surrogates[tokens]); + } + } + + // must be an array of suffix tokens + const qs_start = script_url.indexOf('?'); + + for (let i = 0; i < tokens.length; i++) { + // do any of the suffix tokens match the script URL? + const token = tokens[i]; + + let match = false; + + if (qs_start == -1) { + if (script_url.endsWith(token)) { + match = true; + } + } else { + if (script_url.endsWith(token, qs_start)) { + match = true; + } + } + + if (match) { + // there is a match, return the surrogate code + return 'data:application/javascript;base64,' + btoa(db.surrogates[token]); + } + } + } + + return false; +} + +const exports = { + getSurrogateURI: getSurrogateURI, +}; + +return exports; +})(); diff --git a/src/js/utils.js b/src/js/utils.js new file mode 100644 index 0000000..1072935 --- /dev/null +++ b/src/js/utils.js @@ -0,0 +1,445 @@ +/* + * 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 URI:false */ + +require.scopes.utils = (function() { + +let mdfp = require("multiDomainFP"); + +/** + * Generic interface to make an XHR request + * + * @param {String} url The url to get + * @param {Function} callback The callback to call after request has finished + * @param {String} method GET/POST + * @param {Object} opts XMLHttpRequest options + */ +function xhrRequest(url, callback, method, opts) { + if (!method) { + method = "GET"; + } + if (!opts) { + opts = {}; + } + + let xhr = new XMLHttpRequest(); + + for (let key in opts) { + if (opts.hasOwnProperty(key)) { + xhr[key] = opts[key]; + } + } + + xhr.onload = function () { + if (xhr.status == 200) { + callback(null, xhr.response); + } else { + let error = { + status: xhr.status, + message: xhr.response, + object: xhr + }; + callback(error, error.message); + } + }; + + // triggered by network problems + xhr.onerror = function () { + callback({ status: 0, message: "", object: xhr }, ""); + }; + + xhr.open(method, url, true); + xhr.send(); +} + +/** + * Converts binary data to base64-encoded text suitable for use in data URIs. + * + * Adapted from https://stackoverflow.com/a/9458996. + * + * @param {ArrayBuffer} buffer binary data + * + * @returns {String} base64-encoded text + */ +function arrayBufferToBase64(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +/** + * Return an array of all subdomains in an FQDN, ordered from the FQDN to the + * eTLD+1. e.g. [a.b.eff.org, b.eff.org, eff.org] + * if 'all' is passed in then the array will include all domain levels, not + * just down to the base domain + * @param {String} fqdn the domain to split + * @param {boolean} all whether to include all domain levels + * @returns {Array} the subdomains + */ +function explodeSubdomains(fqdn, all) { + var baseDomain; + if (all) { + baseDomain = fqdn.split('.').pop(); + } else { + baseDomain = window.getBaseDomain(fqdn); + } + var baseLen = baseDomain.split('.').length; + var parts = fqdn.split('.'); + var numLoops = parts.length - baseLen; + var subdomains = []; + for (var i=0; i<=numLoops; i++) { + subdomains.push(parts.slice(i).join('.')); + } + return subdomains; +} + +/* + * Estimates the max possible entropy of string. + * + * @param {String} str the string to compute entropy for + * @returns {Integer} bits of entropy + */ +function estimateMaxEntropy(str) { + // Don't process strings longer than MAX_LS_LEN_FOR_ENTROPY_EST. + // Note that default quota for local storage is 5MB and + // storing fonts, scripts or images in for local storage for + // performance is not uncommon. We wouldn't want to estimate entropy + // for 5M chars. + const MAX_LS_LEN_FOR_ENTROPY_EST = 256; + + // common classes of characters that a string might belong to + const SEPS = "._-x"; + const BIN = "01"; + const DEC = "0123456789"; + + // these classes are case-insensitive + const HEX = "abcdef" + DEC; + const ALPHA = "abcdefghijklmnopqrstuvwxyz"; + const ALPHANUM = ALPHA + DEC; + + // these classes are case-sensitive + const B64 = ALPHANUM + ALPHA.toUpperCase() + "/+"; + const URL = ALPHANUM + ALPHA.toUpperCase() + "~%"; + + if (str.length > MAX_LS_LEN_FOR_ENTROPY_EST) { + // Just return a higher-than-threshold entropy estimate. + // We assume 1 bit per char, which will be well over the + // threshold (33 bits). + return str.length; + } + + let max_symbols; + + // If all characters are upper or lower case, don't consider case when + // computing entropy. + let sameCase = (str.toLowerCase() == str) || (str.toUpperCase() == str); + if (sameCase) { + str = str.toLowerCase(); + } + + // If all the characters come from one of these common character groups, + // assume that the group is the domain of possible characters. + for (let chr_class of [BIN, DEC, HEX, ALPHA, ALPHANUM, B64, URL]) { + let group = chr_class + SEPS; + // Ignore separator characters when computing entropy. For example, Google + // Analytics IDs look like "14103492.1964907". + + // flag to check if each character of input string belongs to the group in question + let each_char_in_group = true; + + for (let ch of str) { + if (!group.includes(ch)) { + each_char_in_group = false; + break; + } + } + + // if the flag resolves to true, we've found our culprit and can break out of the loop + if (each_char_in_group) { + max_symbols = chr_class.length; + break; + } + } + + // If there's not an obvious class of characters, use the heuristic + // "max char code - min char code" + if (!max_symbols) { + let charCodes = Array.prototype.map.call(str, function (ch) { + return String.prototype.charCodeAt.apply(ch); + }); + let min_char_code = Math.min.apply(Math, charCodes); + let max_char_code = Math.max.apply(Math, charCodes); + max_symbols = max_char_code - min_char_code + 1; + } + + // the entropy is (entropy per character) * (number of characters) + let max_bits = (Math.log(max_symbols) / Math.LN2) * str.length; + + return max_bits; +} + +function oneSecond() { + return 1000; +} + +function oneMinute() { + return oneSecond() * 60; +} + +function oneHour() { + return oneMinute() * 60; +} + +function oneDay() { + return oneHour() * 24; +} + +function nDaysFromNow(n) { + return Date.now() + (oneDay() * n); +} + +function oneDayFromNow() { + return nDaysFromNow(1); +} + +/** + * Creates a rate-limited function that delays invoking `fn` until after + * `interval` milliseconds have elapsed since the last time the rate-limited + * function was invoked. + * + * Does not drop invocations (lossless), unlike `_.throttle`. + * + * Adapted from + * http://stackoverflow.com/questions/23072815/throttle-javascript-function-calls-but-with-queuing-dont-discard-calls + * + * @param {Function} fn The function to rate-limit. + * @param {number} interval The number of milliseconds to rate-limit invocations to. + * @param {Object} context The context object (optional). + * @returns {Function} Returns the new rate-limited function. + */ +function rateLimit(fn, interval, context) { + let canInvoke = true, + queue = [], + timer_id, + limited = function () { + queue.push({ + context: context || this, + arguments: Array.prototype.slice.call(arguments) + }); + if (canInvoke) { + canInvoke = false; + timeEnd(); + } + }; + + function timeEnd() { + let item; + if (queue.length) { + item = queue.splice(0, 1)[0]; + fn.apply(item.context, item.arguments); // invoke fn + timer_id = window.setTimeout(timeEnd, interval); + } else { + canInvoke = true; + } + } + + // useful for debugging + limited.cancel = function () { + window.clearTimeout(timer_id); + queue = []; + canInvoke = true; + }; + + return limited; +} + +function buf2hex(buffer) { // buffer is an ArrayBuffer + return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); +} + +function sha1(input, callback) { + return window.crypto.subtle.digest( + { name: "SHA-1", }, + new TextEncoder().encode(input) + ).then(hashed => { + return callback(buf2hex(hashed)); + }); +} + +function parseCookie(str, opts) { + if (!str) { + return {}; + } + + opts = opts || {}; + + let COOKIE_ATTRIBUTES = [ + "domain", + "expires", + "httponly", + "max-age", + "path", + "samesite", + "secure", + ]; + + let parsed = {}, + cookies = str.replace(/\n/g, ";").split(";"); + + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i], + name, + value, + cut = cookie.indexOf("="); + + // it's a key=value pair + if (cut != -1) { + name = cookie.slice(0, cut).trim(); + value = cookie.slice(cut + 1).trim(); + + // handle value quoting + if (value[0] == '"') { + value = value.slice(1, -1); + } + + // not a key=value pair + } else { + if (opts.skipNonValues) { + continue; + } + name = cookie.trim(); + value = ""; + } + + if (opts.skipAttributes && + COOKIE_ATTRIBUTES.indexOf(name.toLowerCase()) != -1) { + continue; + } + + if (!opts.noDecode) { + let decode = opts.decode || decodeURIComponent; + try { + name = decode(name); + } catch (e) { + // invalid URL encoding probably (URIError: URI malformed) + if (opts.skipInvalid) { + continue; + } + } + if (value) { + try { + value = decode(value); + } catch (e) { + // ditto + if (opts.skipInvalid) { + continue; + } + } + } + } + + if (!opts.noOverwrite || !parsed.hasOwnProperty(name)) { + parsed[name] = value; + } + } + + return parsed; +} + +function getHostFromDomainInput(input) { + if (!input.startsWith("http")) { + input = "http://" + input; + } + + if (!input.endsWith("/")) { + input += "/"; + } + + try { + var uri = new URI(input); + } catch (err) { + return false; + } + + return uri.host; +} + +/** + * check if a domain is third party + * @param {String} domain1 an fqdn + * @param {String} domain2 a second fqdn + * + * @return {Boolean} true if the domains are third party + */ +function isThirdPartyDomain(domain1, domain2) { + if (window.isThirdParty(domain1, domain2)) { + return !mdfp.isMultiDomainFirstParty( + window.getBaseDomain(domain1), + window.getBaseDomain(domain2) + ); + } + return false; +} + + +/** + * Checks whether a given URL is a special browser page. + * TODO account for browser-specific pages: + * https://github.com/hackademix/noscript/blob/a8b35486571933043bb62e90076436dff2a34cd2/src/lib/restricted.js + * + * @param {String} url + * + * @return {Boolean} whether the URL is restricted + */ +function isRestrictedUrl(url) { + // permitted schemes from + // https://developer.chrome.com/extensions/match_patterns + return !( + url.startsWith('http') || url.startsWith('file') || url.startsWith('ftp') + ); +} + +/************************************** exports */ +let exports = { + arrayBufferToBase64, + estimateMaxEntropy, + explodeSubdomains, + getHostFromDomainInput, + isRestrictedUrl, + isThirdPartyDomain, + nDaysFromNow, + oneDay, + oneDayFromNow, + oneHour, + oneMinute, + oneSecond, + parseCookie, + rateLimit, + sha1, + xhrRequest, +}; +return exports; +/************************************** exports */ +})(); //require scopes diff --git a/src/js/webrequest.js b/src/js/webrequest.js new file mode 100644 index 0000000..bb7469b --- /dev/null +++ b/src/js/webrequest.js @@ -0,0 +1,1293 @@ +/* + * + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2016 Electronic Frontier Foundation + * + * Derived from Adblock Plus + * Copyright (C) 2006-2013 Eyeo GmbH + * + * Derived from Chameleon <https://github.com/ghostwords/chameleon> + * Copyright (C) 2015 ghostwords + * + * 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 */ + +require.scopes.webrequest = (function () { + +/*********************** webrequest scope **/ + +let constants = require("constants"), + getSurrogateURI = require("surrogates").getSurrogateURI, + incognito = require("incognito"), + utils = require("utils"); + +/************ Local Variables *****************/ +let tempAllowlist = {}; + +/***************** Blocking Listener Functions **************/ + +/** + * Event handling of http requests, main logic to collect data what to block + * + * @param {Object} details The event details + * @returns {Object} Can cancel requests + */ +function onBeforeRequest(details) { + let frame_id = details.frameId, + tab_id = details.tabId, + type = details.type, + url = details.url; + + if (type == "main_frame") { + let oldTabData = badger.getFrameData(tab_id), + is_reload = oldTabData && oldTabData.url == url; + forgetTab(tab_id, is_reload); + badger.recordFrame(tab_id, frame_id, url); + initializeAllowedWidgets(tab_id, badger.getFrameData(tab_id).host); + return {}; + } + + if (type == "sub_frame") { + badger.recordFrame(tab_id, frame_id, url); + } + + // Block ping requests sent by navigator.sendBeacon (see, #587) + // tabId for pings are always -1 due to Chrome bugs #522124 and #522129 + // Once these bugs are fixed, PB will treat pings as any other request + if (type == "ping" && tab_id < 0) { + return {cancel: true}; + } + + if (_isTabChromeInternal(tab_id)) { + return {}; + } + + let tab_host = getHostForTab(tab_id); + let request_host = window.extractHostFromURL(url); + + if (!utils.isThirdPartyDomain(request_host, tab_host)) { + return {}; + } + + let action = checkAction(tab_id, request_host, frame_id); + if (!action) { + return {}; + } + + badger.logThirdPartyOriginOnTab(tab_id, request_host, action); + + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + return {}; + } + + if (action != constants.BLOCK && action != constants.USER_BLOCK) { + return {}; + } + + if (type == 'script') { + let surrogate = getSurrogateURI(url, request_host); + if (surrogate) { + return {redirectUrl: surrogate}; + } + } + + // notify the widget replacement content script + chrome.tabs.sendMessage(tab_id, { + replaceWidget: true, + trackerDomain: request_host + }); + + // if this is a heuristically- (not user-) blocked domain + if (action == constants.BLOCK && incognito.learningEnabled(tab_id)) { + // check for DNT policy asynchronously + setTimeout(function () { + badger.checkForDNTPolicy(request_host); + }, 0); + } + + if (type == 'sub_frame') { + setTimeout(function () { + hideBlockedFrame(tab_id, details.parentFrameId, url, request_host); + }, 0); + } + + return {cancel: true}; +} + +/** + * Filters outgoing cookies and referer + * Injects DNT + * + * @param {Object} details Event details + * @returns {Object} modified headers + */ +function onBeforeSendHeaders(details) { + let frame_id = details.frameId, + tab_id = details.tabId, + type = details.type, + url = details.url; + + if (_isTabChromeInternal(tab_id)) { + // DNT policy requests: strip cookies + if (type == "xmlhttprequest" && url.endsWith("/.well-known/dnt-policy.txt")) { + // remove Cookie headers + let newHeaders = []; + for (let i = 0, count = details.requestHeaders.length; i < count; i++) { + let header = details.requestHeaders[i]; + if (header.name.toLowerCase() != "cookie") { + newHeaders.push(header); + } + } + return { + requestHeaders: newHeaders + }; + } + + return {}; + } + + let tab_host = getHostForTab(tab_id); + let request_host = window.extractHostFromURL(url); + + if (!utils.isThirdPartyDomain(request_host, tab_host)) { + if (badger.isPrivacyBadgerEnabled(tab_host)) { + // Still sending Do Not Track even if HTTP and cookie blocking are disabled + if (badger.isDNTSignalEnabled()) { + details.requestHeaders.push({name: "DNT", value: "1"}, {name: "Sec-GPC", value: "1"}); + } + return {requestHeaders: details.requestHeaders}; + } else { + return {}; + } + } + + let action = checkAction(tab_id, request_host, frame_id); + + if (action) { + badger.logThirdPartyOriginOnTab(tab_id, request_host, action); + } + + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + return {}; + } + + // handle cookieblocked requests + if (action == constants.COOKIEBLOCK || action == constants.USER_COOKIEBLOCK) { + let newHeaders; + + // GET requests: remove cookie headers, reduce referrer header to origin + if (details.method == "GET") { + newHeaders = details.requestHeaders.filter(header => { + return (header.name.toLowerCase() != "cookie"); + }).map(header => { + if (header.name.toLowerCase() == "referer") { + header.value = header.value.slice( + 0, + header.value.indexOf('/', header.value.indexOf('://') + 3) + ) + '/'; + } + return header; + }); + + // remove cookie and referrer headers otherwise + } else { + newHeaders = details.requestHeaders.filter(header => { + return (header.name.toLowerCase() != "cookie" && header.name.toLowerCase() != "referer"); + }); + } + + // add DNT header + if (badger.isDNTSignalEnabled()) { + newHeaders.push({name: "DNT", value: "1"}, {name: "Sec-GPC", value: "1"}); + } + + return {requestHeaders: newHeaders}; + } + + // if we are here, we're looking at a third-party request + // that's not yet blocked or cookieblocked + if (badger.isDNTSignalEnabled()) { + details.requestHeaders.push({name: "DNT", value: "1"}, {name: "Sec-GPC", value: "1"}); + } + return {requestHeaders: details.requestHeaders}; +} + +/** + * Filters incoming cookies out of the response header + * + * @param {Object} details The event details + * @returns {Object} The new response headers + */ +function onHeadersReceived(details) { + let tab_id = details.tabId, + url = details.url; + + if (_isTabChromeInternal(tab_id)) { + // DNT policy responses: strip cookies, reject redirects + if (details.type == "xmlhttprequest" && url.endsWith("/.well-known/dnt-policy.txt")) { + // if it's a redirect, cancel it + if (details.statusCode >= 300 && details.statusCode < 400) { + return { + cancel: true + }; + } + + // remove Set-Cookie headers + let headers = details.responseHeaders, + newHeaders = []; + for (let i = 0, count = headers.length; i < count; i++) { + if (headers[i].name.toLowerCase() != "set-cookie") { + newHeaders.push(headers[i]); + } + } + return { + responseHeaders: newHeaders + }; + } + + return {}; + } + + let tab_host = getHostForTab(tab_id); + let response_host = window.extractHostFromURL(url); + + if (!utils.isThirdPartyDomain(response_host, tab_host)) { + return {}; + } + + let action = checkAction(tab_id, response_host, details.frameId); + if (!action) { + return {}; + } + + badger.logThirdPartyOriginOnTab(tab_id, response_host, action); + + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + return {}; + } + + if (action == constants.COOKIEBLOCK || action == constants.USER_COOKIEBLOCK) { + let newHeaders = details.responseHeaders.filter(function(header) { + return (header.name.toLowerCase() != "set-cookie"); + }); + return {responseHeaders: newHeaders}; + } +} + +/*************** Non-blocking listener functions ***************/ + +/** + * Event handler when a tab gets removed + * + * @param {Integer} tabId Id of the tab + */ +function onTabRemoved(tabId) { + forgetTab(tabId); +} + +/** + * Update internal db on tabs when a tab gets replaced + * due to prerendering or instant search. + * + * @param {Integer} addedTabId The new tab id that replaces + * @param {Integer} removedTabId The tab id that gets removed + */ +function onTabReplaced(addedTabId, removedTabId) { + forgetTab(removedTabId); + // Update the badge of the added tab, which was probably used for prerendering. + badger.updateBadge(addedTabId); +} + +/** + * We don't always get a "main_frame" details object in onBeforeRequest, + * so we need a fallback for (re)initializing tabData. + */ +function onNavigate(details) { + const tab_id = details.tabId, + url = details.url; + + // main (top-level) frames only + if (details.frameId !== 0) { + return; + } + + let oldTabData = badger.getFrameData(tab_id), + is_reload = oldTabData && oldTabData.url == url; + + forgetTab(tab_id, is_reload); + + // forget but don't initialize on special browser/extension pages + if (utils.isRestrictedUrl(url)) { + return; + } + + badger.recordFrame(tab_id, 0, url); + + let tab_host = badger.getFrameData(tab_id).host; + + initializeAllowedWidgets(tab_id, tab_host); + + // initialize tab data bookkeeping used by heuristicBlockingAccounting() + // to avoid missing or misattributing learning + // when there is no "main_frame" webRequest callback + // (such as on Service Worker pages) + // + // see the tabOrigins TODO in heuristicblocking.js + // as to why we don't just use tabData + let base = window.getBaseDomain(tab_host); + badger.heuristicBlocking.tabOrigins[tab_id] = base; + badger.heuristicBlocking.tabUrls[tab_id] = url; +} + +/******** Utility Functions **********/ + +/** + * Messages collapser.js content script to hide blocked frames. + */ +function hideBlockedFrame(tab_id, parent_frame_id, frame_url, frame_host) { + // don't hide if hiding is disabled + if (!badger.getSettings().getItem('hideBlockedElements')) { + return; + } + + // don't hide widget frames + if (badger.isWidgetReplacementEnabled()) { + let exceptions = badger.getSettings().getItem('widgetReplacementExceptions'); + for (let widget of badger.widgetList) { + if (exceptions.includes(widget.name)) { + continue; + } + for (let domain of widget.domains) { + if (domain == frame_host) { + return; + } else if (domain[0] == "*") { // leading wildcard domain + if (frame_host.endsWith(domain.slice(1))) { + return; + } + } + } + } + } + + // message content script + chrome.tabs.sendMessage(tab_id, { + hideFrame: true, + url: frame_url + }, { + frameId: parent_frame_id + }, function (response) { + if (response) { + // content script was ready and received our message + return; + } + // content script was not ready + if (chrome.runtime.lastError) { + // ignore + } + // record frame_url and parent_frame_id + // for when content script becomes ready + let tabData = badger.tabData[tab_id]; + if (!tabData.blockedFrameUrls.hasOwnProperty(parent_frame_id)) { + tabData.blockedFrameUrls[parent_frame_id] = []; + } + tabData.blockedFrameUrls[parent_frame_id].push(frame_url); + }); +} + +/** + * Gets the host name for a given tab id + * @param {Integer} tabId chrome tab id + * @return {String} the host name for the tab + */ +function getHostForTab(tabId) { + let mainFrameIdx = 0; + if (!badger.tabData[tabId]) { + return ''; + } + // TODO what does this actually do? + // meant to address https://github.com/EFForg/privacybadger/issues/136 + if (_isTabAnExtension(tabId)) { + // If the tab is an extension get the url of the first frame for its implied URL + // since the url of frame 0 will be the hash of the extension key + mainFrameIdx = Object.keys(badger.tabData[tabId].frames)[1] || 0; + } + let frameData = badger.getFrameData(tabId, mainFrameIdx); + if (!frameData) { + return ''; + } + return frameData.host; +} + +/** + * Record "supercookie" tracking + * + * @param {Integer} tab_id browser tab ID + * @param {String} frame_url URL of the frame with supercookie + */ +function recordSupercookie(tab_id, frame_url) { + const frame_host = window.extractHostFromURL(frame_url), + page_host = badger.getFrameData(tab_id).host; + + if (!utils.isThirdPartyDomain(frame_host, page_host)) { + // Only happens on the start page for google.com + return; + } + + badger.heuristicBlocking.updateTrackerPrevalence( + frame_host, + window.getBaseDomain(frame_host), + window.getBaseDomain(page_host) + ); +} + +/** + * Record canvas fingerprinting + * + * @param {Integer} tabId the tab ID + * @param {Object} msg specific fingerprinting data + */ +function recordFingerprinting(tabId, msg) { + // Abort if we failed to determine the originating script's URL + // TODO find and fix where this happens + if (!msg.scriptUrl) { + return; + } + + // Ignore first-party scripts + let script_host = window.extractHostFromURL(msg.scriptUrl), + document_host = badger.getFrameData(tabId).host; + if (!utils.isThirdPartyDomain(script_host, document_host)) { + return; + } + + let CANVAS_WRITE = { + fillText: true, + strokeText: true + }; + let CANVAS_READ = { + getImageData: true, + toDataURL: true + }; + + if (!badger.tabData[tabId].hasOwnProperty('fpData')) { + badger.tabData[tabId].fpData = {}; + } + + let script_origin = window.getBaseDomain(script_host); + + // Initialize script TLD-level data + if (!badger.tabData[tabId].fpData.hasOwnProperty(script_origin)) { + badger.tabData[tabId].fpData[script_origin] = { + canvas: { + fingerprinting: false, + write: false + } + }; + } + let scriptData = badger.tabData[tabId].fpData[script_origin]; + + if (msg.extra.hasOwnProperty('canvas')) { + if (scriptData.canvas.fingerprinting) { + return; + } + + // If this script already had a canvas write... + if (scriptData.canvas.write) { + // ...and if this is a canvas read... + if (CANVAS_READ.hasOwnProperty(msg.prop)) { + // ...and it got enough data... + if (msg.extra.width > 16 && msg.extra.height > 16) { + // ...we will classify it as fingerprinting + scriptData.canvas.fingerprinting = true; + log(script_host, 'caught fingerprinting on', document_host); + + // Mark this as a strike + badger.heuristicBlocking.updateTrackerPrevalence( + script_host, script_origin, window.getBaseDomain(document_host)); + } + } + // This is a canvas write + } else if (CANVAS_WRITE.hasOwnProperty(msg.prop)) { + scriptData.canvas.write = true; + } + } +} + +/** + * Cleans up tab-specific data. + * + * @param {Integer} tab_id the ID of the tab + * @param {Boolean} is_reload whether the page is simply being reloaded + */ +function forgetTab(tab_id, is_reload) { + delete badger.tabData[tab_id]; + if (!is_reload) { + delete tempAllowlist[tab_id]; + } +} + +/** + * Determines the action to take on a specific FQDN. + * + * @param {Integer} tabId The relevant tab + * @param {String} requestHost The FQDN + * @param {Integer} frameId The id of the frame + * @returns {(String|Boolean)} false or the action to take + */ +function checkAction(tabId, requestHost, frameId) { + // Ignore requests from temporarily unblocked widgets. + // Someone clicked the widget, so let it load. + if (allowedOnTab(tabId, requestHost, frameId)) { + return false; + } + + // Ignore requests from private domains. + if (window.isPrivateDomain(requestHost)) { + return false; + } + + return badger.storage.getBestAction(requestHost); +} + +/** + * Checks if the tab is chrome internal + * + * @param {Integer} tabId Id of the tab to test + * @returns {boolean} Returns true if the tab is chrome internal + * @private + */ +function _isTabChromeInternal(tabId) { + if (tabId < 0) { + return true; + } + + let frameData = badger.getFrameData(tabId); + if (!frameData || !frameData.url.startsWith("http")) { + return true; + } + + return false; +} + +/** + * Checks if the tab is a chrome-extension tab + * + * @param {Integer} tabId Id of the tab to test + * @returns {boolean} Returns true if the tab is from a chrome-extension + * @private + */ +function _isTabAnExtension(tabId) { + let frameData = badger.getFrameData(tabId); + return (frameData && ( + frameData.url.startsWith("chrome-extension://") || + frameData.url.startsWith("moz-extension://") + )); +} + +/** + * Provides the widget replacing content script with list of widgets to replace. + * + * @param {Integer} tab_id the ID of the tab we're replacing widgets in + * + * @returns {Object} dict containing the complete list of widgets + * as well as a mapping to indicate which ones should be replaced + */ +let getWidgetList = (function () { + // cached translations + let translations; + + // inputs to chrome.i18n.getMessage() + const widgetTranslations = [ + { + key: "social_tooltip_pb_has_replaced", + placeholders: ["XXX"] + }, + { + key: "widget_placeholder_pb_has_replaced", + placeholders: ["XXX"] + }, + { key: "allow_once" }, + { key: "allow_on_site" }, + ]; + + return function (tab_id) { + // an object with keys set to widget names that should be replaced + let widgetsToReplace = {}, + widgetList = [], + tabData = badger.tabData[tab_id], + tabOrigins = tabData && tabData.origins && Object.keys(tabData.origins), + exceptions = badger.getSettings().getItem('widgetReplacementExceptions'); + + // optimize translation lookups by doing them just once, + // the first time they are needed + if (!translations) { + translations = widgetTranslations.reduce((memo, data) => { + memo[data.key] = chrome.i18n.getMessage(data.key, data.placeholders); + return memo; + }, {}); + + // TODO duplicated in src/lib/i18n.js + const RTL_LOCALES = ['ar', 'he', 'fa'], + UI_LOCALE = chrome.i18n.getMessage('@@ui_locale'); + translations.rtl = RTL_LOCALES.indexOf(UI_LOCALE) > -1; + } + + for (let widget of badger.widgetList) { + // replace only if the widget is not on the 'do not replace' list + // also don't send widget data used later for dynamic replacement + if (exceptions.includes(widget.name)) { + continue; + } + + widgetList.push(widget); + + // replace only if at least one of the associated domains was blocked + if (!tabOrigins || !tabOrigins.length) { + continue; + } + let replace = widget.domains.some(domain => { + // leading wildcard domain + if (domain[0] == "*") { + domain = domain.slice(1); + // get all domains in tabData.origins that end with this domain + let matches = tabOrigins.filter(origin => { + return origin.endsWith(domain); + }); + // do we have any matches and are they all blocked? + return matches.length && matches.every(origin => { + const action = tabData.origins[origin]; + return ( + action == constants.BLOCK || + action == constants.USER_BLOCK + ); + }); + } + + // regular, non-leading wildcard domain + if (!tabData.origins.hasOwnProperty(domain)) { + return false; + } + const action = tabData.origins[domain]; + return ( + action == constants.BLOCK || + action == constants.USER_BLOCK + ); + + }); + if (replace) { + widgetsToReplace[widget.name] = true; + } + } + + return { + translations, + widgetList, + widgetsToReplace + }; + }; +}()); + +/** + * Checks if given request FQDN is temporarily unblocked on a tab. + * + * The request is allowed if any of the following is true: + * + * - 1a) Request FQDN matches an entry on the exception list for the tab + * - 1b) Request FQDN ends with a wildcard entry from the exception list + * - 2a) Request is from a subframe whose FQDN matches an entry on the list + * - 2b) Same but subframe's FQDN ends with a wildcard entry + * + * @param {Integer} tab_id the ID of the tab to check + * @param {String} request_host the request FQDN to check + * @param {Integer} frame_id the frame ID to check + * + * @returns {Boolean} true if FQDN is on the temporary allow list + */ +function allowedOnTab(tab_id, request_host, frame_id) { + if (!tempAllowlist.hasOwnProperty(tab_id)) { + return false; + } + + let exceptions = tempAllowlist[tab_id]; + + for (let exception of exceptions) { + if (exception == request_host) { + return true; // 1a + // leading wildcard + } else if (exception[0] == "*") { + if (request_host.endsWith(exception.slice(1))) { + return true; // 1b + } + } + } + + if (!frame_id) { + return false; + } + let frameData = badger.getFrameData(tab_id, frame_id); + if (!frameData || !frameData.host) { + return false; + } + + let frame_host = frameData.host; + for (let exception of exceptions) { + if (exception == frame_host) { + return true; // 2a + // leading wildcard + } else if (exception[0] == "*") { + if (frame_host.endsWith(exception.slice(1))) { + return true; // 2b + } + } + } + + return false; +} + +/** + * @returns {Array|Boolean} the list of associated domains or false + */ +function getWidgetDomains(widget_name) { + let widgetData = badger.widgetList.find( + widget => widget.name == widget_name); + + if (!widgetData || + !widgetData.hasOwnProperty("replacementButton") || + !widgetData.replacementButton.unblockDomains) { + return false; + } + + return widgetData.replacementButton.unblockDomains; +} + +/** + * Marks a set of (widget) domains to be (temporarily) allowed on a tab. + * + * @param {Integer} tab_id the ID of the tab + * @param {Array} domains the domains + */ +function allowOnTab(tab_id, domains) { + if (!tempAllowlist.hasOwnProperty(tab_id)) { + tempAllowlist[tab_id] = []; + } + for (let domain of domains) { + if (!tempAllowlist[tab_id].includes(domain)) { + tempAllowlist[tab_id].push(domain); + } + } +} + +/** + * Called upon navigation to prepopulate the temporary allowlist + * with domains for widgets marked as always allowed on a given site. + */ +function initializeAllowedWidgets(tab_id, tab_host) { + let allowedWidgets = badger.getSettings().getItem('widgetSiteAllowlist'); + if (allowedWidgets.hasOwnProperty(tab_host)) { + for (let widget_name of allowedWidgets[tab_host]) { + let widgetDomains = getWidgetDomains(widget_name); + if (widgetDomains) { + allowOnTab(tab_id, widgetDomains); + } + } + } +} + +// NOTE: sender.tab is available for content script (not popup) messages only +function dispatcher(request, sender, sendResponse) { + + // messages from content scripts are to be treated with greater caution: + // https://groups.google.com/a/chromium.org/d/msg/chromium-extensions/0ei-UCHNm34/lDaXwQhzBAAJ + if (!sender.url.startsWith(chrome.runtime.getURL(""))) { + // reject unless it's a known content script message + const KNOWN_CONTENT_SCRIPT_MESSAGES = [ + "allowWidgetOnSite", + "checkDNT", + "checkEnabled", + "checkLocation", + "checkWidgetReplacementEnabled", + "detectFingerprinting", + "fpReport", + "getBlockedFrameUrls", + "getReplacementButton", + "inspectLocalStorage", + "supercookieReport", + "unblockWidget", + ]; + if (!KNOWN_CONTENT_SCRIPT_MESSAGES.includes(request.type)) { + console.error("Rejected unknown message %o from %s", request, sender.url); + return sendResponse(); + } + } + + switch (request.type) { + + case "checkEnabled": { + sendResponse(badger.isPrivacyBadgerEnabled( + window.extractHostFromURL(sender.tab.url) + )); + + break; + } + + case "checkLocation": { + if (!badger.isPrivacyBadgerEnabled(window.extractHostFromURL(sender.tab.url))) { + return sendResponse(); + } + + // Ignore requests from internal Chrome tabs. + if (_isTabChromeInternal(sender.tab.id)) { + return sendResponse(); + } + + let frame_host = window.extractHostFromURL(request.frameUrl), + tab_host = window.extractHostFromURL(sender.tab.url); + + // Ignore requests that aren't from a third party. + if (!frame_host || !utils.isThirdPartyDomain(frame_host, tab_host)) { + return sendResponse(); + } + + let action = checkAction(sender.tab.id, frame_host); + sendResponse(action == constants.COOKIEBLOCK || action == constants.USER_COOKIEBLOCK); + + break; + } + + case "getBlockedFrameUrls": { + if (!badger.isPrivacyBadgerEnabled(window.extractHostFromURL(sender.tab.url))) { + return sendResponse(); + } + let tab_id = sender.tab.id, + frame_id = sender.frameId, + tabData = badger.tabData.hasOwnProperty(tab_id) && badger.tabData[tab_id], + blockedFrameUrls = tabData && + tabData.blockedFrameUrls.hasOwnProperty(frame_id) && + tabData.blockedFrameUrls[frame_id]; + sendResponse(blockedFrameUrls); + break; + } + + case "unblockWidget": { + let widgetDomains = getWidgetDomains(request.widgetName); + if (!widgetDomains) { + return sendResponse(); + } + allowOnTab(sender.tab.id, widgetDomains); + sendResponse(); + break; + } + + case "allowWidgetOnSite": { + // record that we always want to activate this widget on this site + let tab_host = window.extractHostFromURL(sender.tab.url), + allowedWidgets = badger.getSettings().getItem('widgetSiteAllowlist'); + if (!allowedWidgets.hasOwnProperty(tab_host)) { + allowedWidgets[tab_host] = []; + } + if (!allowedWidgets[tab_host].includes(request.widgetName)) { + allowedWidgets[tab_host].push(request.widgetName); + badger.getSettings().setItem('widgetSiteAllowlist', allowedWidgets); + } + sendResponse(); + break; + } + + case "getReplacementButton": { + let widgetData = badger.widgetList.find( + widget => widget.name == request.widgetName); + if (!widgetData || + !widgetData.hasOwnProperty("replacementButton") || + !widgetData.replacementButton.imagePath) { + return sendResponse(); + } + + let button_path = chrome.runtime.getURL( + "skin/socialwidgets/" + widgetData.replacementButton.imagePath); + + let image_type = button_path.slice(button_path.lastIndexOf('.') + 1); + + let xhrOptions = {}; + if (image_type != "svg") { + xhrOptions.responseType = "arraybuffer"; + } + + // fetch replacement button image data + utils.xhrRequest(button_path, function (err, response) { + // one data URI for SVGs + if (image_type == "svg") { + return sendResponse('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(response)); + } + + // another data URI for all other image formats + sendResponse( + 'data:image/' + image_type + ';base64,' + + utils.arrayBufferToBase64(response) + ); + }, "GET", xhrOptions); + + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "fpReport": { + if (Array.isArray(request.data)) { + request.data.forEach(function (msg) { + recordFingerprinting(sender.tab.id, msg); + }); + } else { + recordFingerprinting(sender.tab.id, request.data); + } + + break; + } + + case "supercookieReport": { + if (request.frameUrl && badger.hasSupercookie(request.data)) { + recordSupercookie(sender.tab.id, request.frameUrl); + } + break; + } + + case "inspectLocalStorage": { + let tab_host = window.extractHostFromURL(sender.tab.url), + frame_host = window.extractHostFromURL(request.frameUrl); + + sendResponse(frame_host && + badger.isLearningEnabled(sender.tab.id) && + badger.isPrivacyBadgerEnabled(tab_host) && + utils.isThirdPartyDomain(frame_host, tab_host)); + + break; + } + + case "detectFingerprinting": { + let tab_host = window.extractHostFromURL(sender.tab.url); + + sendResponse( + badger.isLearningEnabled(sender.tab.id) && + badger.isPrivacyBadgerEnabled(tab_host)); + + break; + } + + case "checkWidgetReplacementEnabled": { + let response = false, + tab_host = window.extractHostFromURL(sender.tab.url); + + if (badger.isPrivacyBadgerEnabled(tab_host) && + badger.isWidgetReplacementEnabled()) { + response = getWidgetList(sender.tab.id); + } + + sendResponse(response); + + break; + } + + case "getPopupData": { + let tab_id = request.tabId; + + if (!badger.tabData.hasOwnProperty(tab_id)) { + sendResponse({ + criticalError: badger.criticalError, + noTabData: true, + seenComic: true, + }); + break; + } + + let tab_host = window.extractHostFromURL(request.tabUrl), + origins = badger.tabData[tab_id].origins, + cookieblocked = {}; + + for (let origin in origins) { + // see if origin would be cookieblocked if not for user override + if (badger.storage.wouldGetCookieblocked(origin)) { + cookieblocked[origin] = true; + } + } + + sendResponse({ + cookieblocked, + criticalError: badger.criticalError, + enabled: badger.isPrivacyBadgerEnabled(tab_host), + errorText: badger.tabData[tab_id].errorText, + learnLocally: badger.getSettings().getItem("learnLocally"), + noTabData: false, + origins, + seenComic: badger.getSettings().getItem("seenComic"), + showLearningPrompt: badger.getPrivateSettings().getItem("showLearningPrompt"), + showNonTrackingDomains: badger.getSettings().getItem("showNonTrackingDomains"), + tabHost: tab_host, + tabId: tab_id, + tabUrl: request.tabUrl, + trackerCount: badger.getTrackerCount(tab_id) + }); + + break; + } + + case "getOptionsData": { + let origins = badger.storage.getTrackingDomains(); + + let cookieblocked = {}; + for (let origin in origins) { + // see if origin would be cookieblocked if not for user override + if (badger.storage.wouldGetCookieblocked(origin)) { + cookieblocked[origin] = true; + } + } + + sendResponse({ + cookieblocked, + isWidgetReplacementEnabled: badger.isWidgetReplacementEnabled(), + origins, + settings: badger.getSettings().getItemClones(), + webRTCAvailable: badger.webRTCAvailable, + widgets: badger.widgetList.map(widget => widget.name), + }); + + break; + } + + case "resetData": { + badger.storage.clearTrackerData(); + badger.loadSeedData(err => { + if (err) { + console.error(err); + } + badger.blockWidgetDomains(); + sendResponse(); + }); + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "removeAllData": { + badger.storage.clearTrackerData(); + sendResponse(); + break; + } + + case "seenComic": { + badger.getSettings().setItem("seenComic", true); + sendResponse(); + break; + } + + case "seenLearningPrompt": { + badger.getPrivateSettings().setItem("showLearningPrompt", false); + sendResponse(); + break; + } + + case "activateOnSite": { + badger.enablePrivacyBadgerForOrigin(request.tabHost); + badger.updateIcon(request.tabId, request.tabUrl); + sendResponse(); + break; + } + + case "deactivateOnSite": { + badger.disablePrivacyBadgerForOrigin(request.tabHost); + badger.updateIcon(request.tabId, request.tabUrl); + sendResponse(); + break; + } + + case "revertDomainControl": { + badger.storage.revertUserAction(request.origin); + sendResponse({ + origins: badger.storage.getTrackingDomains() + }); + break; + } + + case "downloadCloud": { + chrome.storage.sync.get("disabledSites", function (store) { + if (chrome.runtime.lastError) { + sendResponse({success: false, message: chrome.runtime.lastError.message}); + } else if (store.hasOwnProperty("disabledSites")) { + let disabledSites = _.union( + badger.getDisabledSites(), + store.disabledSites + ); + badger.getSettings().setItem("disabledSites", disabledSites); + sendResponse({ + success: true, + disabledSites + }); + } else { + sendResponse({ + success: false, + message: chrome.i18n.getMessage("download_cloud_no_data") + }); + } + }); + + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "uploadCloud": { + let obj = {}; + obj.disabledSites = badger.getDisabledSites(); + chrome.storage.sync.set(obj, function () { + if (chrome.runtime.lastError) { + sendResponse({success: false, message: chrome.runtime.lastError.message}); + } else { + sendResponse({success: true}); + } + }); + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "savePopupToggle": { + let domain = request.origin, + action = request.action; + + badger.saveAction(action, domain); + + // update cached tab data so that a reopened popup displays correct state + badger.tabData[request.tabId].origins[domain] = "user_" + action; + + break; + } + + case "saveOptionsToggle": { + // called when the user manually sets a slider on the options page + badger.saveAction(request.action, request.origin); + sendResponse({ + origins: badger.storage.getTrackingDomains() + }); + break; + } + + case "mergeUserData": { + // called when a user uploads data exported from another Badger instance + badger.mergeUserData(request.data); + badger.blockWidgetDomains(); + sendResponse({ + disabledSites: badger.getDisabledSites(), + origins: badger.storage.getTrackingDomains(), + }); + break; + } + + case "updateSettings": { + const settings = badger.getSettings(); + for (let key in request.data) { + if (badger.defaultSettings.hasOwnProperty(key)) { + settings.setItem(key, request.data[key]); + } else { + console.error("Unknown Badger setting:", key); + } + } + sendResponse(); + break; + } + + case "updateBadge": { + let tab_id = request.tab_id; + badger.updateBadge(tab_id); + sendResponse(); + break; + } + + case "disablePrivacyBadgerForOrigin": { + badger.disablePrivacyBadgerForOrigin(request.domain); + sendResponse({ + disabledSites: badger.getDisabledSites() + }); + break; + } + + case "enablePrivacyBadgerForOriginList": { + request.domains.forEach(function (domain) { + badger.enablePrivacyBadgerForOrigin(domain); + }); + sendResponse({ + disabledSites: badger.getDisabledSites() + }); + break; + } + + case "removeOrigin": { + badger.storage.getStore("snitch_map").deleteItem(request.origin); + badger.storage.getStore("action_map").deleteItem(request.origin); + sendResponse({ + origins: badger.storage.getTrackingDomains() + }); + break; + } + + case "saveErrorText": { + let activeTab = badger.tabData[request.tabId]; + activeTab.errorText = request.errorText; + break; + } + + case "removeErrorText": { + let activeTab = badger.tabData[request.tabId]; + delete activeTab.errorText; + break; + } + + case "checkDNT": { + // called from contentscripts/dnt.js to check if we should enable it + sendResponse( + badger.isDNTSignalEnabled() + && badger.isPrivacyBadgerEnabled( + window.extractHostFromURL(sender.tab.url) + ) + ); + break; + } + + } +} + +/*************** Event Listeners *********************/ +function startListeners() { + chrome.webNavigation.onBeforeNavigate.addListener(onNavigate); + + chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["http://*/*", "https://*/*"]}, ["blocking"]); + + let extraInfoSpec = ['requestHeaders', 'blocking']; + if (chrome.webRequest.OnBeforeSendHeadersOptions.hasOwnProperty('EXTRA_HEADERS')) { + extraInfoSpec.push('extraHeaders'); + } + chrome.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["http://*/*", "https://*/*"]}, extraInfoSpec); + + extraInfoSpec = ['responseHeaders', 'blocking']; + if (chrome.webRequest.OnHeadersReceivedOptions.hasOwnProperty('EXTRA_HEADERS')) { + extraInfoSpec.push('extraHeaders'); + } + chrome.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["<all_urls>"]}, extraInfoSpec); + + chrome.tabs.onRemoved.addListener(onTabRemoved); + chrome.tabs.onReplaced.addListener(onTabReplaced); + chrome.runtime.onMessage.addListener(dispatcher); +} + +/************************************** exports */ +let exports = { + startListeners +}; +return exports; +/************************************** exports */ +})(); |