/* 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/. */

const UPDATE_BEGIN = "safebrowsing-update-begin";
const UPDATE_FINISH = "safebrowsing-update-finished";
const JSLOG_PREF = "browser.safebrowsing.debug";

window.onunload = function () {
  Search.uninit();
  Provider.uninit();
  Cache.uninit();
  Debug.uninit();
};

window.onload = function () {
  Search.init();
  Provider.init();
  Cache.init();
  Debug.init();
};

/*
 * Search
 */
var Search = {
  init() {
    let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
      Ci.nsIURIClassifier
    );
    let featureNames = classifier.getFeatureNames();

    let fragment = document.createDocumentFragment();
    featureNames.forEach(featureName => {
      let container = document.createElement("label");
      container.className = "toggle-container-with-text";
      fragment.appendChild(container);

      let checkbox = document.createElement("input");
      checkbox.id = "feature_" + featureName;
      checkbox.type = "checkbox";
      checkbox.checked = true;
      container.appendChild(checkbox);

      let span = document.createElement("span");
      container.appendChild(span);

      let text = document.createTextNode(featureName);
      span.appendChild(text);
    });

    let list = document.getElementById("search-features");
    list.appendChild(fragment);

    let btn = document.getElementById("search-button");
    btn.addEventListener("click", this.search);

    this.hideError();
    this.hideResults();
  },

  uninit() {
    let list = document.getElementById("search-features");
    while (list.firstChild) {
      list.firstChild.remove();
    }

    let btn = document.getElementById("search-button");
    btn.removeEventListener("click", this.search);
  },

  search() {
    Search.hideError();
    Search.hideResults();

    let input = document.getElementById("search-input").value;

    let uri;
    try {
      uri = Services.io.newURI(input);
      if (!uri) {
        Search.reportError("url-classifier-search-error-invalid-url");
        return;
      }
    } catch (ex) {
      Search.reportError("url-classifier-search-error-invalid-url");
      return;
    }

    let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
      Ci.nsIURIClassifier
    );

    let featureNames = classifier.getFeatureNames();
    let features = [];
    featureNames.forEach(featureName => {
      if (document.getElementById("feature_" + featureName).checked) {
        let feature = classifier.getFeatureByName(featureName);
        if (feature) {
          features.push(feature);
        }
      }
    });

    if (!features.length) {
      Search.reportError("url-classifier-search-error-no-features");
      return;
    }

    let listType =
      document.getElementById("search-listtype").value == 0
        ? Ci.nsIUrlClassifierFeature.blocklist
        : Ci.nsIUrlClassifierFeature.entitylist;
    classifier.asyncClassifyLocalWithFeatures(uri, features, listType, list =>
      Search.showResults(list)
    );

    Search.hideError();
  },

  hideError() {
    let errorMessage = document.getElementById("search-error-message");
    errorMessage.style.display = "none";
  },

  reportError(msg) {
    let errorMessage = document.getElementById("search-error-message");
    document.l10n.setAttributes(errorMessage, msg);
    errorMessage.style.display = "";
  },

  hideResults() {
    let resultTitle = document.getElementById("result-title");
    resultTitle.style.display = "none";

    let resultTable = document.getElementById("result-table");
    resultTable.style.display = "none";
  },

  showResults(results) {
    let fragment = document.createDocumentFragment();
    results.forEach(result => {
      let tr = document.createElement("tr");
      fragment.appendChild(tr);

      let th = document.createElement("th");
      tr.appendChild(th);
      th.appendChild(document.createTextNode(result.feature.name));

      let td = document.createElement("td");
      tr.appendChild(td);

      let featureName = document.createElement("div");
      document.l10n.setAttributes(
        featureName,
        "url-classifier-search-result-uri",
        { uri: result.uri.spec }
      );
      td.appendChild(featureName);

      let list = document.createElement("div");
      document.l10n.setAttributes(list, "url-classifier-search-result-list", {
        list: result.list,
      });
      td.appendChild(list);
    });

    let resultTable = document.getElementById("result-table");
    while (resultTable.firstChild) {
      resultTable.firstChild.remove();
    }

    resultTable.appendChild(fragment);
    resultTable.style.display = "";

    let resultTitle = document.getElementById("result-title");
    resultTitle.style.display = "";
  },
};

/*
 * Provider
 */
var Provider = {
  providers: null,

  updatingProvider: "",

  init() {
    this.providers = new Set();
    let branch = Services.prefs.getBranch("browser.safebrowsing.provider.");
    let children = branch.getChildList("");
    for (let child of children) {
      let provider = child.split(".")[0];
      if (this.isActiveProvider(provider)) {
        this.providers.add(provider);
      }
    }

    this.register();
    this.render();
    this.refresh();
  },

  uninit() {
    Services.obs.removeObserver(this.onBeginUpdate, UPDATE_BEGIN);
    Services.obs.removeObserver(this.onFinishUpdate, UPDATE_FINISH);
  },

  onBeginUpdate(aSubject, aTopic, aData) {
    this.updatingProvider = aData;
    let p = this.updatingProvider;

    // Disable update button for the provider while we are doing update.
    document.getElementById("update-" + p).disabled = true;

    let elem = document.getElementById(p + "-col-lastupdateresult");
    document.l10n.setAttributes(elem, "url-classifier-updating");
  },

  onFinishUpdate(aSubject, aTopic, aData) {
    let p = this.updatingProvider;
    this.updatingProvider = "";

    // It is possible that we get update-finished event only because
    // about::url-classifier is opened after update-begin event is fired.
    if (p === "") {
      this.refresh();
      return;
    }

    this.refresh([p]);

    document.getElementById("update-" + p).disabled = false;

    let elem = document.getElementById(p + "-col-lastupdateresult");
    if (aData.startsWith("success")) {
      document.l10n.setAttributes(elem, "url-classifier-success");
    } else if (aData.startsWith("update error")) {
      document.l10n.setAttributes(elem, "url-classifier-update-error", {
        error: aData.split(": ")[1],
      });
    } else if (aData.startsWith("download error")) {
      document.l10n.setAttributes(elem, "url-classifier-download-error", {
        error: aData.split(": ")[1],
      });
    } else {
      elem.childNodes[0].nodeValue = aData;
    }
  },

  register() {
    // Handle begin update
    this.onBeginUpdate = this.onBeginUpdate.bind(this);
    Services.obs.addObserver(this.onBeginUpdate, UPDATE_BEGIN);

    // Handle finish update
    this.onFinishUpdate = this.onFinishUpdate.bind(this);
    Services.obs.addObserver(this.onFinishUpdate, UPDATE_FINISH);
  },

  // This should only be called once because we assume number of providers
  // won't change.
  render() {
    let tbody = document.getElementById("provider-table-body");

    for (let provider of this.providers) {
      let tr = document.createElement("tr");
      let cols = document.getElementById("provider-head-row").childNodes;
      for (let column of cols) {
        if (!column.id) {
          continue;
        }
        let td = document.createElement("td");
        td.id = provider + "-" + column.id;

        if (column.id === "col-update") {
          let btn = document.createElement("button");
          btn.id = "update-" + provider;
          btn.addEventListener("click", () => {
            this.update(provider);
          });

          document.l10n.setAttributes(btn, "url-classifier-trigger-update");
          td.appendChild(btn);
        } else if (column.id === "col-lastupdateresult") {
          document.l10n.setAttributes(td, "url-classifier-not-available");
        } else {
          td.appendChild(document.createTextNode(""));
        }
        tr.appendChild(td);
      }
      tbody.appendChild(tr);
    }
  },

  refresh(listProviders = this.providers) {
    for (let provider of listProviders) {
      let values = {};
      values["col-provider"] = provider;

      let pref =
        "browser.safebrowsing.provider." + provider + ".lastupdatetime";
      let lut = Services.prefs.getCharPref(pref, "");
      values["col-lastupdatetime"] = lut ? new Date(lut * 1) : null;

      pref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
      let nut = Services.prefs.getCharPref(pref, "");
      values["col-nextupdatetime"] = nut ? new Date(nut * 1) : null;

      let listmanager = Cc[
        "@mozilla.org/url-classifier/listmanager;1"
      ].getService(Ci.nsIUrlListManager);
      let bot = listmanager.getBackOffTime(provider);
      values["col-backofftime"] = bot ? new Date(bot * 1) : null;

      for (let key of Object.keys(values)) {
        let elem = document.getElementById(provider + "-" + key);
        if (values[key]) {
          elem.removeAttribute("data-l10n-id");
          elem.childNodes[0].nodeValue = values[key];
        } else {
          document.l10n.setAttributes(elem, "url-classifier-not-available");
        }
      }
    }
  },

  // Call update for the provider.
  update(provider) {
    let listmanager = Cc[
      "@mozilla.org/url-classifier/listmanager;1"
    ].getService(Ci.nsIUrlListManager);

    let pref = "browser.safebrowsing.provider." + provider + ".lists";
    let tables = Services.prefs.getCharPref(pref, "");

    if (!listmanager.forceUpdates(tables)) {
      // This may because of back-off algorithm.
      let elem = document.getElementById(provider + "-col-lastupdateresult");
      document.l10n.setAttributes(elem, "url-classifier-cannot-update");
    }
  },

  // if we can find any table registered an updateURL in the listmanager,
  // the provider is active. This is used to filter out google v2 provider
  // without changing the preference.
  isActiveProvider(provider) {
    let listmanager = Cc[
      "@mozilla.org/url-classifier/listmanager;1"
    ].getService(Ci.nsIUrlListManager);

    let pref = "browser.safebrowsing.provider." + provider + ".lists";
    let tables = Services.prefs.getCharPref(pref, "").split(",");

    for (let i = 0; i < tables.length; i++) {
      let updateUrl = listmanager.getUpdateUrl(tables[i]);
      if (updateUrl) {
        return true;
      }
    }

    return false;
  },
};

/*
 * Cache
 */
var Cache = {
  // Tables that show cahe entries.
  showCacheEnties: null,

  init() {
    this.showCacheEnties = new Set();

    this.register();
    this.render();
  },

  uninit() {
    Services.obs.removeObserver(this.refresh, UPDATE_FINISH);
  },

  register() {
    this.refresh = this.refresh.bind(this);
    Services.obs.addObserver(this.refresh, UPDATE_FINISH);
  },

  render() {
    this.createCacheEntries();

    let refreshBtn = document.getElementById("refresh-cache-btn");
    refreshBtn.addEventListener("click", () => {
      this.refresh();
    });

    let clearBtn = document.getElementById("clear-cache-btn");
    clearBtn.addEventListener("click", () => {
      let dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
        Ci.nsIUrlClassifierDBService
      );
      dbservice.clearCache();
      // Since clearCache is async call, we just simply assume it will be
      // updated in 100 milli-seconds.
      setTimeout(() => {
        this.refresh();
      }, 100);
    });
  },

  refresh() {
    this.clearCacheEntries();
    this.createCacheEntries();
  },

  clearCacheEntries() {
    let ctbody = document.getElementById("cache-table-body");
    while (ctbody.firstChild) {
      ctbody.firstChild.remove();
    }

    let cetbody = document.getElementById("cache-entries-table-body");
    while (cetbody.firstChild) {
      cetbody.firstChild.remove();
    }
  },

  createCacheEntries() {
    function createRow(tds, body, cols) {
      let tr = document.createElement("tr");
      tds.forEach(function (v, i) {
        let td = document.createElement("td");
        if (i == 0 && tds.length != cols) {
          td.setAttribute("colspan", cols - tds.length + 1);
        }

        if (typeof v === "object") {
          if (v.l10n) {
            document.l10n.setAttributes(td, v.l10n);
          } else {
            td.removeAttribute("data-l10n-id");
            td.appendChild(v);
          }
        } else {
          td.removeAttribute("data-l10n-id");
          td.textContent = v;
        }

        tr.appendChild(td);
      });
      body.appendChild(tr);
    }

    let dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
      Ci.nsIUrlClassifierInfo
    );

    for (let provider of Provider.providers) {
      let pref = "browser.safebrowsing.provider." + provider + ".lists";
      let tables = Services.prefs.getCharPref(pref, "").split(",");

      for (let table of tables) {
        dbservice.getCacheInfo(table, {
          onGetCacheComplete: aCache => {
            let entries = aCache.entries;
            if (entries.length === 0) {
              this.showCacheEnties.delete(table);
              return;
            }

            let positiveCacheCount = 0;
            for (let i = 0; i < entries.length; i++) {
              let entry = entries.queryElementAt(
                i,
                Ci.nsIUrlClassifierCacheEntry
              );
              let matches = entry.matches;
              positiveCacheCount += matches.length;

              // If we don't have to show cache entries for this table then just
              // skip the following code.
              if (!this.showCacheEnties.has(table)) {
                continue;
              }

              let tds = [
                table,
                entry.prefix,
                new Date(entry.expiry * 1000).toString(),
              ];
              let j = 0;
              do {
                if (matches.length >= 1) {
                  let match = matches.queryElementAt(
                    j,
                    Ci.nsIUrlClassifierPositiveCacheEntry
                  );
                  let list = [
                    match.fullhash,
                    new Date(match.expiry * 1000).toString(),
                  ];
                  tds = tds.concat(list);
                } else {
                  tds = tds.concat([
                    { l10n: "url-classifier-not-available" },
                    { l10n: "url-classifier-not-available" },
                  ]);
                }
                createRow(
                  tds,
                  document.getElementById("cache-entries-table-body"),
                  5
                );
                j++;
                tds = [""];
              } while (j < matches.length);
            }

            // Create cache information entries.
            let chk = document.createElement("input");
            chk.type = "checkbox";
            chk.checked = this.showCacheEnties.has(table);
            chk.addEventListener("click", () => {
              if (chk.checked) {
                this.showCacheEnties.add(table);
              } else {
                this.showCacheEnties.delete(table);
              }
              this.refresh();
            });

            let tds = [table, entries.length, positiveCacheCount, chk];
            createRow(
              tds,
              document.getElementById("cache-table-body"),
              tds.length
            );
          },
        });
      }
    }

    let entries_div = document.getElementById("cache-entries");
    entries_div.style.display =
      this.showCacheEnties.size == 0 ? "none" : "block";
  },
};

/*
 * Debug
 */
var Debug = {
  // url-classifier NSPR Log modules.
  modules: [
    "UrlClassifierDbService",
    "nsChannelClassifier",
    "UrlClassifier",
    "UrlClassifierProtocolParser",
    "UrlClassifierStreamUpdater",
    "UrlClassifierPrefixSet",
    "ApplicationReputation",
  ],

  init() {
    this.register();
    this.render();
    this.refresh();
  },

  uninit() {
    Services.prefs.removeObserver(JSLOG_PREF, this.refreshJSDebug);
  },

  register() {
    this.refreshJSDebug = this.refreshJSDebug.bind(this);
    Services.prefs.addObserver(JSLOG_PREF, this.refreshJSDebug);
  },

  render() {
    // This function update the log module text field if we click
    // safebrowsing log module check box.
    function logModuleUpdate(module) {
      let txt = document.getElementById("log-modules");
      let chk = document.getElementById("chk-" + module);

      let dst = chk.checked ? "," + module + ":5" : "";
      let re = new RegExp(",?" + module + ":[0-9]");

      let str = txt.value.replace(re, dst);
      if (chk.checked) {
        str = txt.value === str ? str + dst : str;
      }
      txt.value = str.replace(/^,/, "");
    }

    let setLog = document.getElementById("set-log-modules");
    setLog.addEventListener("click", this.nsprlog);

    let setLogFile = document.getElementById("set-log-file");
    setLogFile.addEventListener("click", this.logfile);

    let setJSLog = document.getElementById("js-log");
    setJSLog.addEventListener("click", this.jslog);

    let modules = document.getElementById("log-modules");
    let sbModules = document.getElementById("sb-log-modules");
    for (let module of this.modules) {
      let container = document.createElement("label");
      container.className = "toggle-container-with-text";
      sbModules.appendChild(container);

      let chk = document.createElement("input");
      chk.id = "chk-" + module;
      chk.type = "checkbox";
      chk.checked = true;
      chk.addEventListener("click", () => {
        logModuleUpdate(module);
      });
      container.appendChild(chk, modules);

      let span = document.createElement("span");
      span.appendChild(document.createTextNode(module));
      container.appendChild(span, modules);
    }

    this.modules.map(logModuleUpdate);

    let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
    file.append("safebrowsing.log");

    let logFile = document.getElementById("log-file");
    logFile.value = file.path;

    let curLog = document.getElementById("cur-log-modules");
    curLog.childNodes[0].nodeValue = "";

    let curLogFile = document.getElementById("cur-log-file");
    curLogFile.childNodes[0].nodeValue = "";
  },

  refresh() {
    this.refreshJSDebug();

    // Disable configure log modules if log modules are already set
    // by environment variable.

    let logModules =
      Services.env.get("MOZ_LOG") ||
      Services.env.get("MOZ_LOG_MODULES") ||
      Services.env.get("NSPR_LOG_MODULES");

    if (logModules.length) {
      document.getElementById("set-log-modules").disabled = true;
      for (let module of this.modules) {
        document.getElementById("chk-" + module).disabled = true;
      }

      let curLogModules = document.getElementById("cur-log-modules");
      curLogModules.childNodes[0].nodeValue = logModules;
    }

    // Disable set log file if log file is already set
    // by environment variable.
    let logFile =
      Services.env.get("MOZ_LOG_FILE") || Services.env.get("NSPR_LOG_FILE");
    if (logFile.length) {
      document.getElementById("set-log-file").disabled = true;
      document.getElementById("log-file").value = logFile;
    }
  },

  refreshJSDebug() {
    let enabled = Services.prefs.getBoolPref(JSLOG_PREF, false);

    let jsChk = document.getElementById("js-log");
    jsChk.checked = enabled;

    let curJSLog = document.getElementById("cur-js-log");
    if (enabled) {
      document.l10n.setAttributes(curJSLog, "url-classifier-enabled");
    } else {
      document.l10n.setAttributes(curJSLog, "url-classifier-disabled");
    }
  },

  jslog() {
    let enabled = Services.prefs.getBoolPref(JSLOG_PREF, false);
    Services.prefs.setBoolPref(JSLOG_PREF, !enabled);
  },

  nsprlog() {
    // Turn off debugging for all the modules.
    let children = Services.prefs.getBranch("logging.").getChildList("");
    for (let pref of children) {
      if (!pref.startsWith("config.")) {
        Services.prefs.clearUserPref(`logging.${pref}`);
      }
    }

    let value = document.getElementById("log-modules").value;
    let logModules = value.split(",");
    for (let module of logModules) {
      let [key, value] = module.split(":");
      Services.prefs.setIntPref(`logging.${key}`, parseInt(value, 10));
    }

    let curLogModules = document.getElementById("cur-log-modules");
    curLogModules.childNodes[0].nodeValue = value;
  },

  logfile() {
    let logFile = document.getElementById("log-file").value.trim();
    Services.prefs.setCharPref("logging.config.LOG_FILE", logFile);

    let curLogFile = document.getElementById("cur-log-file");
    curLogFile.childNodes[0].nodeValue = logFile;
  },
};
