/*
 * 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();
