summaryrefslogtreecommitdiffstats
path: root/src/js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 19:47:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 19:47:39 +0000
commit8d13bdc6cac0e20c43c6f909fc0208774b9c5c84 (patch)
tree5fd46925c6b4a881c9208772ed8e5cc0588bc164 /src/js
parentInitial commit. (diff)
downloadprivacybadger-8d13bdc6cac0e20c43c6f909fc0208774b9c5c84.tar.xz
privacybadger-8d13bdc6cac0e20c43c6f909fc0208774b9c5c84.zip
Adding upstream version 2020.10.7.upstream/2020.10.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js')
-rw-r--r--src/js/background.js1148
-rw-r--r--src/js/bootstrap.js37
-rw-r--r--src/js/constants.js54
-rw-r--r--src/js/contentscripts/clobbercookie.js60
-rw-r--r--src/js/contentscripts/clobberlocalstorage.js94
-rw-r--r--src/js/contentscripts/collapser.js56
-rw-r--r--src/js/contentscripts/dnt.js66
-rw-r--r--src/js/contentscripts/fingerprinting.js367
-rw-r--r--src/js/contentscripts/socialwidgets.js641
-rw-r--r--src/js/contentscripts/supercookie.js151
-rw-r--r--src/js/contentscripts/utils.js53
-rw-r--r--src/js/firefoxandroid.js90
-rw-r--r--src/js/firstparties/facebook.js56
-rw-r--r--src/js/firstparties/google-search.js39
-rw-r--r--src/js/firstparties/google-static.js41
-rw-r--r--src/js/firstparties/lib/utils.js62
-rw-r--r--src/js/heuristicblocking.js557
-rw-r--r--src/js/htmlutils.js283
-rw-r--r--src/js/incognito.js49
-rw-r--r--src/js/migrations.js356
-rw-r--r--src/js/multiDomainFirstParties.js4133
-rw-r--r--src/js/options.js976
-rw-r--r--src/js/popup.js723
-rw-r--r--src/js/socialwidgetloader.js133
-rw-r--r--src/js/storage.js707
-rw-r--r--src/js/surrogates.js87
-rw-r--r--src/js/utils.js445
-rw-r--r--src/js/webrequest.js1293
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">&#10006</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 */
+})();