"use strict";

(function(exports) {

const rules = require('./rules'),
  store = require('./store'),
  incognito = require('./incognito'),
  util = require('./util'),
  update = require('./update'),
  { update_channels } = require('./update_channels'),
  wasm = require('./wasm'),
  ipUtils = require('./ip_utils');


let all_rules = new rules.RuleSets();

async function initialize() {
  await wasm.initialize();
  await store.initialize();
  await store.performMigrations();
  await initializeStoredGlobals();
  await getUpgradeToSecureAvailable();
  await update.initialize(store, initializeAllRules);
  await all_rules.loadFromBrowserStorage(store, update.applyStoredRulesets);
  await incognito.onIncognitoDestruction(destroy_caches);
}
initialize();

async function initializeAllRules() {
  const r = new rules.RuleSets();
  await r.loadFromBrowserStorage(store, update.applyStoredRulesets);
  Object.assign(all_rules, r);
}

/**
 * Load preferences. Structure is:
 *  {
 *    httpNowhere: Boolean,
 *    isExtensionEnabled: Boolean
 *  }
 */
var httpNowhereOn = false;
var isExtensionEnabled = true;
let disabledList = new Set();
let httpOnceList = new Set();

/**
 * Check if HTTPS Everywhere should be ON for host
 */
function isExtensionDisabledOnSite(host) {
  // make sure the host is not matched in the httpOnceList
  if (httpOnceList.has(host)) {
    return true;
  }

  // make sure the host is not matched in the disabledList
  if (disabledList.has(host)) {
    return true;
  }

  // make sure the host is matched by any wildcard expressions in the disabledList
  const experessions = util.getWildcardExpressions(host);
  for (const expression of experessions) {
    if (disabledList.has(expression)) {
      return true;
    }
  }

  // otherwise return false
  return false;
}

function initializeStoredGlobals() {
  return new Promise(resolve => {
    store.get({
      httpNowhere: false,
      globalEnabled: true,
      enableMixedRulesets: false,
      disabledList: []
    }, function(item) {
      httpNowhereOn = item.httpNowhere;
      isExtensionEnabled = item.globalEnabled;
      for (let disabledSite of item.disabledList) {
        disabledList.add(disabledSite);
      }
      updateState();

      rules.settings.enableMixedRulesets = item.enableMixedRulesets;

      resolve();
    });
  });
}

let upgradeToSecureAvailable;

function getUpgradeToSecureAvailable() {
  if (typeof browser !== 'undefined') {
    return browser.runtime.getBrowserInfo().then(function(info) {
      let version = info.version.match(/^(\d+)/)[1];
      if (info.name == "Firefox" && version >= 59) {
        upgradeToSecureAvailable = true;
      } else {
        upgradeToSecureAvailable = false;
      }
    });
  } else {
    return new Promise(resolve => {
      upgradeToSecureAvailable = false;
      resolve();
    });
  }
}

chrome.storage.onChanged.addListener(async function(changes, areaName) {
  if (areaName === 'sync' || areaName === 'local') {
    if ('httpNowhere' in changes) {
      httpNowhereOn = changes.httpNowhere.newValue;
      updateState();
    }
    if ('globalEnabled' in changes) {
      isExtensionEnabled = changes.globalEnabled.newValue;
      updateState();
    }
    if ('enableMixedRulesets' in changes) {
      // Don't require users to restart the browsers
      rules.settings.enableMixedRulesets = changes.enableMixedRulesets.newValue;
      initializeAllRules();
    }
    if ('debugging_rulesets' in changes) {
      initializeAllRules();
    }
  }
});

if (chrome.tabs) {
  chrome.tabs.onActivated.addListener(function() {
    updateState();
  });
}
if (chrome.windows) {
  chrome.windows.onFocusChanged.addListener(function() {
    updateState();
  });

  // Grant access to HTTP site only during session, clear once window is closed
  chrome.windows.onRemoved.addListener(function() {
    chrome.windows.getAll({}, function(windows) {
      if(windows.length > 0) {
        return;
      } else {
        httpOnceList.clear();
      }
    });
  });

}
chrome.webNavigation.onCompleted.addListener(function() {
  updateState();
});

/**
 * Set the icon color correctly
 * active: extension is enabled.
 * blocking: extension is in "block all HTTP requests" mode.
 * disabled: extension is disabled from the popup menu.
 */

function updateState () {
  if (!chrome.tabs) return;

  let iconState = 'active';

  if (!isExtensionEnabled) {
    iconState = 'disabled';
  } else if (httpNowhereOn) {
    iconState = 'blocking';
  }

  chrome.browserAction.setTitle({
    title: 'HTTPS Everywhere' + ((iconState === 'active') ? '' : ' (' + iconState + ')')
  });

  chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
    if (!tabs || tabs.length === 0) {
      return;
    }
    const tabUrl = new URL(tabs[0].url);
    const hostname = util.getNormalisedHostname(tabUrl.hostname);

    if (isExtensionDisabledOnSite(hostname) || iconState == "disabled") {
      if ('setIcon' in chrome.browserAction) {
        chrome.browserAction.setIcon({
          path: {
            38: 'images/icons/icon-disabled-38.png'
          }
        });
      }
    } else {
      if ('setIcon' in chrome.browserAction) {
        chrome.browserAction.setIcon({
          path: {
            38: 'images/icons/icon-' + iconState + '-38.png'
          }
        });
      }
    }
  });
}

/**
 * The following allows fennec to interact with the popup ui
 * */
chrome.browserAction.onClicked.addListener(e => {
  const url = chrome.runtime.getURL("/pages/popup/index.html?tabId=" + e.id);
  chrome.tabs.create({
    url
  });
});

/**
 * A centralized storage for browsing data within the browser session.
 */
function BrowserSession() {
  this.tabs = new Map();
  this.requests = new Map();

  if (chrome.tabs) {
    chrome.tabs.onRemoved.addListener(tabId => {
      this.deleteTab(tabId);
    });
  }
}

BrowserSession.prototype = {
  putTab: function(tabId, key, value, overwrite) {
    if (!this.tabs.has(tabId)) {
      this.tabs.set(tabId, {});
    }

    if (!(key in this.tabs.get(tabId)) || overwrite) {
      this.tabs.get(tabId)[key] = value;
    }
  },

  getTab: function(tabId, key, defaultValue) {
    if (this.tabs.has(tabId) && key in this.tabs.get(tabId)) {
      return this.tabs.get(tabId)[key];
    }
    return defaultValue;
  },

  deleteTab: function(tabId) {
    if (this.tabs.has(tabId)) {
      this.tabs.delete(tabId);
    }
  },

  putTabAppliedRulesets: function(tabId, type, ruleset) {
    this.putTab(tabId, "main_frame", false, false);

    // always show main_frame ruleset on the top
    if (type == "main_frame") {
      this.putTab(tabId, "main_frame", true, true);
      this.putTab(tabId, "applied_rulesets", [ruleset,], true);
      return ;
    }

    // sort by ruleset names alphabetically, case-insensitive
    if (this.getTab(tabId, "applied_rulesets", null)) {
      let rulesets = this.getTab(tabId, "applied_rulesets");
      let insertIndex = 0;

      const ruleset_name = ruleset.name.toLowerCase();

      for (const item of rulesets) {
        const item_name = item.name.toLowerCase();

        if (item_name == ruleset_name) {
          return ;
        } else if (insertIndex == 0 && this.getTab(tabId, "main_frame", false)) {
          insertIndex = 1;
        } else if (item_name < ruleset_name) {
          insertIndex++;
        }
      }
      rulesets.splice(insertIndex, 0, ruleset);
    } else {
      this.putTab(tabId, "applied_rulesets", [ruleset,], true);
    }
  },

  getTabAppliedRulesets: function(tabId) {
    return this.getTab(tabId, "applied_rulesets", null);
  },

  putRequest: function(requestId, key, value) {
    if (!this.requests.has(requestId)) {
      this.requests.set(requestId, {});
    }
    this.requests.get(requestId)[key] = value;
  },

  getRequest: function(requestId, key, defaultValue) {
    if (this.requests.has(requestId) && key in this.requests.get(requestId)) {
      return this.requests.get(requestId)[key];
    }
    return defaultValue;
  },

  deleteRequest: function(requestId) {
    if (this.requests.has(requestId)) {
      this.requests.delete(requestId);
    }
  }
};

let browserSession = new BrowserSession();

var urlBlacklist = new Set();

const cancelUrl = chrome.runtime.getURL("/pages/cancel/index.html");

function redirectOnCancel(shouldCancel, originURL) {
  return shouldCancel ? {redirectUrl: newCancelUrl(originURL)} : {cancel: false};
}

const newCancelUrl = originURL => `${cancelUrl}?originURL=${encodeURIComponent(originURL)}`;

/**
 * Called before a HTTP(s) request. Does the heavy lifting
 * Cancels the request/redirects it to HTTPS. URL modification happens in here.
 * @param details of the handler, see Chrome doc
 * */
function onBeforeRequest(details) {
  // If HTTPSe has been disabled by the user, return immediately.
  if (!isExtensionEnabled) {
    return;
  }

  let uri = new URL(details.url);

  // Normalise hosts with tailing dots, e.g. "www.example.com."
  uri.hostname = util.getNormalisedHostname(uri.hostname);

  let ip = ipUtils.parseIp(uri.hostname);

  let isLocalIp = false;

  if (ip !== -1) {
    isLocalIp = ipUtils.isLocalIp(ip);
  }

  if (details.type == "main_frame") {
    // Clear the content from previous browser session.
    // This needed to be done before this listener returns,
    // otherwise, the extension popup might include rulesets
    // from previous page.
    browserSession.deleteTab(details.tabId);

    // Check if an user has disabled HTTPS Everywhere on this site.  We should
    // ensure that all subresources are not run through HTTPS Everywhere as well.
    browserSession.putTab(details.tabId, 'first_party_host', uri.hostname, true);
  }

  if (isExtensionDisabledOnSite(browserSession.getTab(details.tabId, 'first_party_host', null))) {
    return;
  }

  // Should the request be canceled?
  // true if the URL is a http:// connection to a remote canonical host, and not
  // a tor hidden service
  const shouldCancel = httpNowhereOn &&
    (uri.protocol === 'http:' || uri.protocol === 'ftp:') &&
    uri.hostname.slice(-6) !== '.onion' &&
    uri.hostname !== 'localhost' &&
    !uri.hostname.endsWith('.localhost') &&
    uri.hostname !== '[::1]' &&
    !isLocalIp;

  // If there is a username / password, put them aside during the ruleset
  // analysis process
  let using_credentials_in_url = false;
  let tmp_user;
  let tmp_pass;
  if (uri.password || uri.username) {
    using_credentials_in_url = true;
    tmp_user = uri.username;
    tmp_pass = uri.password;
    uri.username = '';
    uri.password = '';
  }

  if (details.url != uri.href && !using_credentials_in_url) {
    util.log(util.INFO, "Original url " + details.url +
        " changed before processing to " + uri.href);
  }
  if (urlBlacklist.has(uri.href)) {
    return redirectOnCancel(shouldCancel, details.url);
  }

  if (browserSession.getRequest(details.requestId, "redirect_count") >= 8) {
    util.log(util.NOTE, "Redirect counter hit for " + uri.href);
    urlBlacklist.add(uri.href);
    rules.settings.domainBlacklist.add(uri.hostname);
    util.log(util.WARN, "Domain blacklisted " + uri.hostname);
    return redirectOnCancel(shouldCancel, details.url);
  }

  // whether to use mozilla's upgradeToSecure BlockingResponse if available
  let upgradeToSecure = false;
  let newuristr = null;

  let potentiallyApplicable = all_rules.potentiallyApplicableRulesets(uri.hostname);

  for (let ruleset of potentiallyApplicable) {
    if (details.url.match(ruleset.scope)) {
      browserSession.putTabAppliedRulesets(details.tabId, details.type, ruleset);
      if (ruleset.active && !newuristr) {
        newuristr = ruleset.apply(uri.href);
      }
    }
  }

  // only use upgradeToSecure for trivial rewrites
  if (upgradeToSecureAvailable && newuristr) {
    // check rewritten URIs against the trivially upgraded URI
    const trivialUpgradeUri = uri.href.replace(/^http:/, "https:");
    upgradeToSecure = (newuristr == trivialUpgradeUri);
  }

  // re-insert userpass info which was stripped temporarily
  if (using_credentials_in_url) {
    if (newuristr) {
      const uri_with_credentials = new URL(newuristr);
      uri_with_credentials.username = tmp_user;
      uri_with_credentials.password = tmp_pass;
      newuristr = uri_with_credentials.href;
    } else {
      const url_with_credentials = new URL(uri.href);
      url_with_credentials.username = tmp_user;
      url_with_credentials.password = tmp_pass;
      uri.href = url_with_credentials.href;
    }
  }

  if (httpNowhereOn) {
    // If loading a main frame, try the HTTPS version as an alternative to
    // failing.
    if (shouldCancel) {
      if (!newuristr) {
        newuristr = uri.href.replace(/^http:/, "https:");
        browserSession.putRequest(details.requestId, "simple_http_nowhere_redirect", true);
        upgradeToSecure = true;
      } else {
        newuristr = newuristr.replace(/^http:/, "https:");
      }
    }
    if (
      newuristr &&
      (
        newuristr.substring(0, 5) === "http:" ||
        newuristr.substring(0, 4) === "ftp:"
      )
    ) {
      // Abort early if we're about to redirect to HTTP or FTP in HTTP Nowhere mode
      return {redirectUrl: newCancelUrl(newuristr)};
    }
  }

  if (upgradeToSecureAvailable && upgradeToSecure) {
    util.log(util.INFO, 'onBeforeRequest returning upgradeToSecure: true');
    return {upgradeToSecure: true};
  } else if (newuristr) {
    util.log(util.INFO, 'onBeforeRequest returning redirectUrl: ' + newuristr);
    return {redirectUrl: newuristr};
  } else {
    util.log(util.INFO, 'onBeforeRequest returning shouldCancel: ' + shouldCancel);
    return redirectOnCancel(shouldCancel, details.url);
  }
}

/**
 * monitor cookie changes. Automatically convert them to secure cookies
 * @param changeInfo Cookie changed info, see Chrome doc
 * */
function onCookieChanged(changeInfo) {
  if (!changeInfo.removed && !changeInfo.cookie.secure && isExtensionEnabled) {
    if (all_rules.shouldSecureCookie(changeInfo.cookie)) {
      let cookie = {
        name:changeInfo.cookie.name,
        value:changeInfo.cookie.value,
        path:changeInfo.cookie.path,
        httpOnly:changeInfo.cookie.httpOnly,
        expirationDate:changeInfo.cookie.expirationDate,
        storeId:changeInfo.cookie.storeId,
        secure: true
      };

      // Host-only cookies don't set the domain field.
      if (!changeInfo.cookie.hostOnly) {
        cookie.domain = changeInfo.cookie.domain;
      }

      // Chromium cookie sameSite status, see https://tools.ietf.org/html/draft-west-first-party-cookies
      if (changeInfo.cookie.sameSite) {
        cookie.sameSite = changeInfo.cookie.sameSite;
      }

      // Firefox first-party isolation
      if (changeInfo.cookie.firstPartyDomain) {
        cookie.firstPartyDomain = changeInfo.cookie.firstPartyDomain;
      }

      // The cookie API is magical -- we must recreate the URL from the domain and path.
      if (changeInfo.cookie.domain[0] == ".") {
        cookie.url = "https://www" + changeInfo.cookie.domain + cookie.path;
      } else {
        cookie.url = "https://" + changeInfo.cookie.domain + cookie.path;
      }
      // We get repeated events for some cookies because sites change their
      // value repeatedly and remove the "secure" flag.
      util.log(util.DBUG,
        "Securing cookie " + cookie.name + " for " + changeInfo.cookie.domain + ", was secure=" + changeInfo.cookie.secure);
      chrome.cookies.set(cookie);
    }
  }
}

/**
 * handling redirects, breaking loops
 * @param details details for the redirect (see chrome doc)
 * */
function onBeforeRedirect(details) {
  // Catch redirect loops (ignoring about:blank, etc. caused by other extensions)
  let prefix = details.redirectUrl.substring(0, 5);
  if (prefix === "http:" || prefix === "https") {
    let count = browserSession.getRequest(details.requestId, "redirect_count", 0);
    if (count) {
      browserSession.putRequest(details.requestId, "redirect_count", count + 1);
      util.log(util.DBUG, "Got redirect id " + details.requestId + ": "+count);
    } else {
      browserSession.putRequest(details.requestId, "redirect_count", 1);
    }
  }
}

/**
 * handle webrequest.onCompleted, cleanup redirectCounter
 * @param details details for the chrome.webRequest (see chrome doc)
 */
function onCompleted(details) {
  browserSession.deleteRequest(details.requestId);
}

/**
 * handle webrequest.onErrorOccurred, cleanup redirectCounter
 * @param details details for the chrome.webRequest (see chrome doc)
 */
function onErrorOccurred(details) {
  if (httpNowhereOn &&
    details.type == "main_frame" &&
    browserSession.getRequest(details.requestId, "simple_http_nowhere_redirect", false) &&
    ( // Enumerate a class of errors that are likely due to HTTPS misconfigurations
      details.error.indexOf("net::ERR_SSL_") == 0 ||
      details.error.indexOf("net::ERR_CERT_") == 0 ||
      details.error.indexOf("net::ERR_CONNECTION_") == 0 ||
      details.error.indexOf("net::ERR_ABORTED") == 0 ||
      details.error.indexOf("net::ERR_SSL_PROTOCOL_ERROR") == 0 ||
      details.error.indexOf("NS_ERROR_CONNECTION_REFUSED") == 0 ||
      details.error.indexOf("NS_ERROR_NET_TIMEOUT") == 0 ||
      details.error.indexOf("NS_ERROR_NET_ON_TLS_HANDSHAKE_ENDED") == 0 ||
      details.error.indexOf("SSL received a record that exceeded the maximum permissible length.") == 0 ||
      details.error.indexOf("Peer’s Certificate has expired.") == 0 ||
      details.error.indexOf("Unable to communicate securely with peer: requested domain name does not match the server’s certificate.") == 0 ||
      details.error.indexOf("Peer’s Certificate issuer is not recognized.") == 0 ||
      details.error.indexOf("Peer’s Certificate has been revoked.") == 0 ||
      details.error.indexOf("Peer reports it experienced an internal error.") == 0 ||
      details.error.indexOf("The server uses key pinning (HPKP) but no trusted certificate chain could be constructed that matches the pinset. Key pinning violations cannot be overridden.") == 0 ||
      details.error.indexOf("SSL received a weak ephemeral Diffie-Hellman key in Server Key Exchange handshake message.") == 0 ||
      details.error.indexOf("The certificate was signed using a signature algorithm that is disabled because it is not secure.") == 0 ||
      details.error.indexOf("Unable to communicate securely with peer: requested domain name does not match the server’s certificate.") == 0 ||
      details.error.indexOf("Cannot communicate securely with peer: no common encryption algorithm(s).") == 0 ||
      details.error.indexOf("SSL peer has no certificate for the requested DNS name.") == 0
    )) {
    let url = new URL(details.url);
    if (url.protocol == "https:") {
      url.protocol = "http:";
    }
    chrome.tabs.update(details.tabId, {url: newCancelUrl(url.toString())});
  }

  browserSession.deleteRequest(details.requestId);
}

/**
 * handle webrequest.onHeadersReceived, insert upgrade-insecure-requests directive and
 * rewrite access-control-allow-origin if presented in HTTP Nowhere mode
 * @param details details for the chrome.webRequest (see chrome doc)
 */
function onHeadersReceived(details) {
  if (isExtensionEnabled && httpNowhereOn) {
    // Do not upgrade the .onion requests in EASE mode,
    // See https://github.com/EFForg/https-everywhere/pull/14600#discussion_r168072480
    const uri = new URL(details.url);
    const hostname = util.getNormalisedHostname(uri.hostname);
    if (hostname.slice(-6) == '.onion') {
      return {};
    }

    // Do not upgrade resources if the first-party domain disbled EASE mode
    // This is needed for HTTPS sites serve mixed content and is broken
    if (isExtensionDisabledOnSite(browserSession.getTab(details.tabId, 'first_party_host', null))) {
      return {};
    }

    let responseHeadersChanged = false;
    let cspHeaderFound = false;

    for (const idx in details.responseHeaders) {
      if (details.responseHeaders[idx].name.match(/Content-Security-Policy/i)) {
        // Existing CSP headers found
        cspHeaderFound = true;
        const value = details.responseHeaders[idx].value;

        // Prepend if no upgrade-insecure-requests directive exists
        if (!value.match(/upgrade-insecure-requests/i)) {
          details.responseHeaders[idx].value = "upgrade-insecure-requests; " + value;
          responseHeadersChanged = true;
        }
      }

      if (details.responseHeaders[idx].name.match(/Access-Control-Allow-Origin/i)) {
        // Existing access-control-allow-origin header found
        const value = details.responseHeaders[idx].value;

        // If HTTP protocol is used, change it to HTTPS
        if (value.match(/http:/)) {
          details.responseHeaders[idx].value = value.replace(/http:/g, "https:");
          responseHeadersChanged = true;
        }
      }
    }

    if (!cspHeaderFound) {
      // CSP headers not found
      const upgradeInsecureRequests = {
        name: 'Content-Security-Policy',
        value: 'upgrade-insecure-requests'
      };
      details.responseHeaders.push(upgradeInsecureRequests);
      responseHeadersChanged = true;
    }

    if (responseHeadersChanged) {
      return {responseHeaders: details.responseHeaders};
    }
  }
  return {};
}

// Registers the handler for requests
// See: https://github.com/EFForg/https-everywhere/issues/10039
chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["*://*/*", "ftp://*/*"]}, ["blocking"]);

// Try to catch redirect loops on URLs we've redirected to HTTPS.
chrome.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["https://*/*"]});

// Cleanup redirectCounter if necessary
chrome.webRequest.onCompleted.addListener(onCompleted, {urls: ["*://*/*"]});

// Cleanup redirectCounter if necessary
chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["*://*/*"]});

// Insert upgrade-insecure-requests directive in httpNowhere mode
chrome.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["https://*/*"]}, ["blocking", "responseHeaders"]);

// Listen for cookies set/updated and secure them if applicable. This function is async/nonblocking.
chrome.cookies.onChanged.addListener(onCookieChanged);

// This is necessary for communication with the popup in Firefox Private
// Browsing Mode, see https://bugzilla.mozilla.org/show_bug.cgi?id=1329304
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {

  function get_update_channels_generic(update_channels) {
    let last_updated_promises = [];
    for(let update_channel of update_channels) {
      last_updated_promises.push(new Promise(resolve => {
        store.local.get({['rulesets-timestamp: ' + update_channel.name]: 0}, item => {
          resolve([update_channel.name, item['rulesets-timestamp: ' + update_channel.name]]);
        });
      }));
    }
    Promise.all(last_updated_promises).then(results => {
      const last_updated = results.reduce((obj, item) => {
        obj[item[0]] = item[1];
        return obj;
      }, {});
      sendResponse({update_channels, last_updated});
    });
  }

  function storeDisabledList(message) {

    const disabledListArray = Array.from(disabledList);
    const httpOnceListArray = Array.from(httpOnceList);

    if (message === 'once') {
      store.set({httpOnceList: httpOnceListArray}, () => {
        sendResponse(true);
      });
    } else {
      store.set({disabledList: disabledListArray}, () => {
        sendResponse(true);
      });
    }

    return true;
  }

  const responses = {
    get_option: () => {
      store.get(message.object, sendResponse);
      return true;
    },
    set_option: () => {
      store.set(message.object, item => {
        if (sendResponse) {
          sendResponse(item);
        }
      });
    },
    delete_from_ruleset_cache: () => {
      all_rules.ruleCache.delete(message.object);
    },
    get_applied_rulesets: () => {
      sendResponse(browserSession.getTabAppliedRulesets(message.object));
      return true;
    },
    set_ruleset_active_status: () => {
      let rulesets = browserSession.getTabAppliedRulesets(message.object.tab_id);

      for (let ruleset of rulesets) {
        if (ruleset.name == message.object.name) {
          ruleset.active = message.object.active;
          if (ruleset.default_state == message.object.active) {
            message.object.active = undefined;
          }
          break;
        }
      }

      all_rules.setRuleActiveState(message.object.name, message.object.active).then(() => {
        sendResponse(true);
      });

      return true;
    },
    reset_to_defaults: () => {
      // restore the 'default states' of the rulesets
      store.set_promise('ruleActiveStates', {}).then(() => {
        // clear the caches such that it becomes stateless
        destroy_caches();
        // re-activate all rules according to the new states
        initializeAllRules();
        // reload tabs when operations completed
        chrome.tabs.reload();
      });
    },
    get_user_rules: () => {
      store.get_promise(all_rules.USER_RULE_KEY, []).then(userRules => sendResponse(userRules));
      return true;
    },
    add_new_rule: () => {
      all_rules.addNewRuleAndStore(message.object).then(() => {
        sendResponse(true);
      });
      return true;
    },
    remove_rule: () => {
      all_rules.removeRuleAndStore(message.object.ruleset, message.object.src)
        .then(() => {
          /**
           * FIXME: initializeAllRules is needed for calls from the option pages.
           * Since message.object is not of type Ruleset, rules.removeUserRule
           * is not usable...
           */
          if (message.object.src === 'options') {
            return initializeAllRules();
          }
        })
        .then(() => {
          if (sendResponse !== null) {
            sendResponse(true);
          }
        });
      return true;
    },
    get_ruleset_timestamps: () => {
      update.getRulesetTimestamps().then(timestamps => sendResponse(timestamps));
      return true;
    },
    get_pinned_update_channels: () => {
      get_update_channels_generic(update_channels);
      return true;
    },
    get_stored_update_channels: () => {
      store.get({update_channels: []}, item => {
        get_update_channels_generic(item.update_channels);
      });
      return true;
    },
    create_update_channel: () => {

      store.get({update_channels: []}, item => {

        const update_channel_names = update_channels.concat(item.update_channels).reduce((obj, item) => {
          obj.add(item.name);
          return obj;
        }, new Set());

        if(update_channel_names.has(message.object)) {
          return sendResponse(false);
        }

        item.update_channels.push({
          name: message.object,
          jwk: {},
          update_path_prefix: '',
          scope: ''
        });

        store.set({update_channels: item.update_channels}, () => {
          sendResponse(true);
        });

      });
      return true;
    },
    delete_update_channel: () => {
      store.get({update_channels: []}, item => {
        store.set({update_channels: item.update_channels.filter(update_channel => {
          return (update_channel.name != message.object);
        })}, () => {
          store.local.remove([
            'rulesets-timestamp: ' + message.object,
            'rulesets-stored-timestamp: ' + message.object,
            'rulesets: ' + message.object
          ], () => {
            initializeAllRules();
            sendResponse(true);
          });
        });
      });
      return true;
    },
    update_update_channel: () => {
      store.get({update_channels: []}, item => {
        let scope_changed = false;
        item.update_channels = item.update_channels.map(update_channel => {
          if(update_channel.name == message.object.name) {
            if(update_channel.scope != message.object.scope) {
              scope_changed = true;
            }
            update_channel = message.object;
          }
          return update_channel;
        });

        // Ensure that we check for new rulesets from the update channel immediately.
        // If the scope has changed, make sure that the rulesets are re-initialized.
        update.removeStorageListener();
        store.set({update_channels: item.update_channels}, () => {
          update.loadUpdateChannelsKeys().then(() => {
            update.resetTimer();
            if(scope_changed) {
              initializeAllRules();
            }
            sendResponse(true);
          });
          update.addStorageListener();
        });
      });
      return true;
    },
    get_simple_rules_ending_with: () => {
      return sendResponse(all_rules.getSimpleRulesEndingWith(message.object));
    },
    get_last_checked: () => {
      store.local.get({'last-checked': false}, item => {
        sendResponse(item['last-checked']);
      });
      return true;
    },
    disable_on_site_once: () => {
      httpOnceList.add(message.object);
      return storeDisabledList('once');
    },
    disable_on_site: () => {
      const host = util.getNormalisedHostname(message.object);
      // always validate hostname before adding it to the disabled list
      if (util.isValidHostname(host)) {
        disabledList.add(host);
        return storeDisabledList('disable');
      }
      return sendResponse(false);
    },
    enable_on_site: () => {
      disabledList.delete(util.getNormalisedHostname(message.object));
      return storeDisabledList('enable');
    },
    check_if_site_disabled: () => {
      return sendResponse(isExtensionDisabledOnSite(util.getNormalisedHostname(message.object)));
    },
    is_firefox: () => {
      if(typeof(browser) != "undefined") {
        browser.runtime.getBrowserInfo().then(function(info) {
          if (info.name == "Firefox") {
            sendResponse(true);
          } else {
            sendResponse(false);
          }
        });
      } else {
        sendResponse(false);
      }
      return true;
    }
  };
  if (message.type in responses) {
    return responses[message.type]();
  }
});

/**
 * Clear any cache/ blacklist we have.
 */
function destroy_caches() {
  util.log(util.DBUG, "Destroying caches.");
  all_rules.cookieHostCache.clear();
  all_rules.ruleCache.clear();
  rules.settings.domainBlacklist.clear();
  urlBlacklist.clear();
  httpOnceList.clear();
}

Object.assign(exports, {
  all_rules,
  urlBlacklist
});

})(typeof exports == 'undefined' ? require.scopes.background = {} : exports);
