/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                  "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                  "resource://gre/modules/Downloads.jsm");

this.EXPORTED_SYMBOLS = ["ForgetAboutSite"];

/**
 * Returns true if the string passed in is part of the root domain of the
 * current string.  For example, if this is "www.mozilla.org", and we pass in
 * "mozilla.org", this will return true.  It would return false the other way
 * around.
 */
function hasRootDomain(str, aDomain)
{
  let index = str.indexOf(aDomain);
  // If aDomain is not found, we know we do not have it as a root domain.
  if (index == -1)
    return false;

  // If the strings are the same, we obviously have a match.
  if (str == aDomain)
    return true;

  // Otherwise, we have aDomain as our root domain iff the index of aDomain is
  // aDomain.length subtracted from our length and (since we do not have an
  // exact match) the character before the index is a dot or slash.
  let prevChar = str[index - 1];
  return (index == (str.length - aDomain.length)) &&
         (prevChar == "." || prevChar == "/");
}

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

this.ForgetAboutSite = {
  removeDataFromDomain: Task.async(function* (aDomain)
  {
    PlacesUtils.history.removePagesFromHost(aDomain, true);

    let promises = [];
    // Cache
    promises.push(Task.spawn(function*() {
      let cs = Cc["@mozilla.org/netwerk/cache-storage-service;1"].
               getService(Ci.nsICacheStorageService);
      // NOTE: there is no way to clear just that domain, so we clear out
      //       everything)
      cs.clear();
    }).catch(ex => {
      throw new Error("Exception thrown while clearing the cache: " + ex);
    }));

    // Image Cache
    promises.push(Task.spawn(function*() {
      let imageCache = Cc["@mozilla.org/image/tools;1"].
                       getService(Ci.imgITools).getImgCacheForDocument(null);
      imageCache.clearCache(false); // true=chrome, false=content
    }).catch(ex => {
      throw new Error("Exception thrown while clearing the image cache: " + ex);
    }));

    // Cookies
    // Need to maximize the number of cookies cleaned here
    promises.push(Task.spawn(function*() {
      let cm = Cc["@mozilla.org/cookiemanager;1"].
               getService(Ci.nsICookieManager2);
      let enumerator = cm.getCookiesWithOriginAttributes(JSON.stringify({}), aDomain);
      while (enumerator.hasMoreElements()) {
        let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
        cm.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
      }
    }).catch(ex => {
      throw new Error("Exception thrown while clearning cookies: " + ex);
    }));

    // EME
    promises.push(Task.spawn(function*() {
      let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].
                getService(Ci.mozIGeckoMediaPluginChromeService);
      mps.forgetThisSite(aDomain, JSON.stringify({}));
    }).catch(ex => {
      throw new Error("Exception thrown while clearing Encrypted Media Extensions: " + ex);
    }));

    // Plugin data
    const phInterface = Ci.nsIPluginHost;
    const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL;
    let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface);
    let tags = ph.getPluginTags();
    for (let i = 0; i < tags.length; i++) {
      promises.push(new Promise(resolve => {
        try {
          ph.clearSiteData(tags[i], aDomain, FLAG_CLEAR_ALL, -1, resolve);
        } catch (e) {
          // Ignore errors from the plugin, but resolve the promise
          // We cannot check if something is a bailout or an error
          resolve();
        }
      }));
    }

    // Downloads
    promises.push(Task.spawn(function*() {
      let list = yield Downloads.getList(Downloads.ALL);
      list.removeFinished(download => hasRootDomain(
        NetUtil.newURI(download.source.url).host, aDomain));
    }).catch(ex => {
      throw new Error("Exception in clearing Downloads: " + ex);
    }));

    // Passwords
    promises.push(Task.spawn(function*() {
      let lm = Cc["@mozilla.org/login-manager;1"].
               getService(Ci.nsILoginManager);
      // Clear all passwords for domain
      let logins = lm.getAllLogins();
      for (let i = 0; i < logins.length; i++) {
        if (hasRootDomain(logins[i].hostname, aDomain)) {
          lm.removeLogin(logins[i]);
        }
      }
    }).catch(ex => {
      // XXX:
      // Is there a better way to do this rather than this hacky comparison?
      // Copied this from toolkit/components/passwordmgr/crypto-SDR.js
      if (!ex.message.includes("User canceled master password entry")) {
        throw new Error("Exception occured in clearing passwords: " + ex);
      }
    }));

    // Permissions
    let pm = Cc["@mozilla.org/permissionmanager;1"].
             getService(Ci.nsIPermissionManager);
    // Enumerate all of the permissions, and if one matches, remove it
    let enumerator = pm.enumerator;
    while (enumerator.hasMoreElements()) {
      let perm = enumerator.getNext().QueryInterface(Ci.nsIPermission);
      promises.push(new Promise((resolve, reject) => {
        try {
          if (hasRootDomain(perm.principal.URI.host, aDomain)) {
            pm.removePermission(perm);
          }
        } catch (ex) {
          // Ignore entry
        } finally {
          resolve();
        }
      }));
    }

    // Offline Storages
    promises.push(Task.spawn(function*() {
      let qms = Cc["@mozilla.org/dom/quota-manager-service;1"].
                getService(Ci.nsIQuotaManagerService);
      // delete data from both HTTP and HTTPS sites
      let httpURI = NetUtil.newURI("http://" + aDomain);
      let httpsURI = NetUtil.newURI("https://" + aDomain);
      // Following code section has been reverted to the state before Bug 1238183,
      // but added a new argument to clearStoragesForPrincipal() for indicating
      // clear all storages under a given origin.
      let httpPrincipal = Services.scriptSecurityManager
                                  .createCodebasePrincipal(httpURI, {});
      let httpsPrincipal = Services.scriptSecurityManager
                                   .createCodebasePrincipal(httpsURI, {});
      qms.clearStoragesForPrincipal(httpPrincipal, null, true);
      qms.clearStoragesForPrincipal(httpsPrincipal, null, true);
    }).catch(ex => {
      throw new Error("Exception occured while clearing offline storages: " + ex);
    }));

    // Content Preferences
    promises.push(Task.spawn(function*() {
      let cps2 = Cc["@mozilla.org/content-pref/service;1"].
                 getService(Ci.nsIContentPrefService2);
      cps2.removeBySubdomain(aDomain, null, {
        handleCompletion: (reason) => {
          // Notify other consumers, including extensions
          Services.obs.notifyObservers(null, "browser:purge-domain-data", aDomain);
          if (reason === cps2.COMPLETE_ERROR) {
            throw new Error("Exception occured while clearing content preferences");
          }
        },
        handleError() {}
      });
    }));

    // Predictive network data - like cache, no way to clear this per
    // domain, so just trash it all
    promises.push(Task.spawn(function*() {
      let np = Cc["@mozilla.org/network/predictor;1"].
               getService(Ci.nsINetworkPredictor);
      np.reset();
    }).catch(ex => {
      throw new Error("Exception occured while clearing predictive network data: " + ex);
    }));

    // Push notifications
    promises.push(Task.spawn(function*() {
      var push = Cc["@mozilla.org/push/Service;1"].
                 getService(Ci.nsIPushService);
      push.clearForDomain(aDomain, status => {
        if (!Components.isSuccessCode(status)) {
          throw new Error("Exception occured while clearing push notifications: " + status);
        }
      });
    }));

    // HSTS and HPKP
    // TODO (bug 1290529): also remove HSTS/HPKP information for subdomains.
    // Since we can't enumerate the information in the site security service
    // (bug 1115712), we can't implement this right now.
    promises.push(Task.spawn(function*() {
      let sss = Cc["@mozilla.org/ssservice;1"].
                getService(Ci.nsISiteSecurityService);
      let httpsURI = NetUtil.newURI("https://" + aDomain);
      sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, httpsURI, 0);
      sss.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, httpsURI, 0);
    }).catch(ex => {
      throw new Error("Exception thrown while clearing HSTS/HPKP: " + ex);
    }));

    let ErrorCount = 0;
    for (let promise of promises) {
      try {
        yield promise;
      } catch (ex) {
        Cu.reportError(ex);
        ErrorCount++;
      }
    }
    if (ErrorCount !== 0)
      throw new Error(`There were a total of ${ErrorCount} errors during removal`);
  })
}