Bug 1348253 - about:url-classifier: Providers information and update button. r?francois draft
authorDimi Lee <dlee@mozilla.com>
Wed, 19 Apr 2017 00:33:08 +0800
changeset 564855 dc67b5b8ff29be8d989765482f0f71d1d8c6cb42
parent 564706 3f9f6d6086b2d247831d1d03d530095bebd5a6b2
child 624838 3a59adf3f40898054355c07dcb091b83fbe71a31
push id54701
push userbmo:dlee@mozilla.com
push dateWed, 19 Apr 2017 03:01:08 +0000
reviewersfrancois
bugs1348253
milestone55.0a1
Bug 1348253 - about:url-classifier: Providers information and update button. r?francois The about:url-classifier supports following functions: 1. Provider section - Show update status for each provider, update status include last update time, next update time and last update status - Update button to manually trigger an update for the provider. 2. Debug section - Set MOZ_LOG Modules - Set MOZ_LOG_FILE MozReview-Commit-ID: AHiveKEHSNC
docshell/base/nsAboutRedirector.cpp
docshell/build/nsDocShellModule.cpp
toolkit/components/url-classifier/content/listmanager.js
toolkit/components/url-classifier/nsIUrlListManager.idl
toolkit/content/aboutUrlClassifier.css
toolkit/content/aboutUrlClassifier.js
toolkit/content/aboutUrlClassifier.xhtml
toolkit/content/jar.mn
toolkit/locales/en-US/chrome/global/aboutUrlClassifier.dtd
toolkit/locales/en-US/chrome/global/aboutUrlClassifier.properties
toolkit/locales/jar.mn
--- a/docshell/base/nsAboutRedirector.cpp
+++ b/docshell/base/nsAboutRedirector.cpp
@@ -131,16 +131,20 @@ static const RedirEntry kRedirMap[] = {
     "support", "chrome://global/content/aboutSupport.xhtml",
     nsIAboutModule::ALLOW_SCRIPT
   },
   {
     "telemetry", "chrome://global/content/aboutTelemetry.xhtml",
     nsIAboutModule::ALLOW_SCRIPT
   },
   {
+    "url-classifier", "chrome://global/content/aboutUrlClassifier.xhtml",
+    nsIAboutModule::ALLOW_SCRIPT
+  },
+  {
     "webrtc", "chrome://global/content/aboutwebrtc/aboutWebrtc.html",
     nsIAboutModule::ALLOW_SCRIPT
   },
   {
     "printpreview", "about:blank",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT |
     nsIAboutModule::URI_CAN_LOAD_IN_CHILD
--- a/docshell/build/nsDocShellModule.cpp
+++ b/docshell/build/nsDocShellModule.cpp
@@ -193,16 +193,17 @@ const mozilla::Module::ContractIDEntry k
 #ifndef ANDROID
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "profiles", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
 #endif
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "srcdoc", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "support", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "telemetry", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "webrtc", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_ABOUT_MODULE_CONTRACTID_PREFIX "printpreview", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
+  { NS_ABOUT_MODULE_CONTRACTID_PREFIX "url-classifier", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
   { NS_URI_LOADER_CONTRACTID, &kNS_URI_LOADER_CID },
   { NS_DOCUMENTLOADER_SERVICE_CONTRACTID, &kNS_DOCUMENTLOADER_SERVICE_CID },
   { NS_HANDLERSERVICE_CONTRACTID, &kNS_CONTENTHANDLERSERVICE_CID, mozilla::Module::CONTENT_PROCESS_ONLY },
   { NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &kNS_EXTERNALHELPERAPPSERVICE_CID },
   { NS_EXTERNALPROTOCOLSERVICE_CONTRACTID, &kNS_EXTERNALHELPERAPPSERVICE_CID },
   { NS_MIMESERVICE_CONTRACTID, &kNS_EXTERNALHELPERAPPSERVICE_CID },
   { NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX"default", &kNS_EXTERNALPROTOCOLHANDLER_CID },
   { NS_PREFETCHSERVICE_CONTRACTID, &kNS_PREFETCHSERVICE_CID },
--- a/toolkit/components/url-classifier/content/listmanager.js
+++ b/toolkit/components/url-classifier/content/listmanager.js
@@ -127,16 +127,23 @@ PROT_ListManager.prototype.registerTable
 
 PROT_ListManager.prototype.getGethashUrl = function(tableName) {
   if (this.tablesData[tableName] && this.tablesData[tableName].gethashUrl) {
     return this.tablesData[tableName].gethashUrl;
   }
   return "";
 }
 
+PROT_ListManager.prototype.getUpdateUrl = function(tableName) {
+  if (this.tablesData[tableName] && this.tablesData[tableName].updateUrl) {
+    return this.tablesData[tableName].updateUrl;
+  }
+  return "";
+}
+
 /**
  * Enable updates for some tables
  * @param tables - an array of table names that need updating
  */
 PROT_ListManager.prototype.enableUpdate = function(tableName) {
   var table = this.tablesData[tableName];
   if (table) {
     log("Enabling table updates for " + tableName);
@@ -478,16 +485,22 @@ PROT_ListManager.prototype.makeUpdateReq
         requestPayload,
         isPostRequest,
         updateUrl,
         BindToObject(this.updateSuccess_, this, tableList, updateUrl),
         BindToObject(this.updateError_, this, tableList, updateUrl),
         BindToObject(this.downloadError_, this, tableList, updateUrl))) {
     // Our alarm gets reset in one of the 3 callbacks.
     log("pending update, queued request until later");
+  } else {
+    let table = Object.keys(this.tablesData).find(key => {
+      return this.tablesData[key].updateUrl === updateUrl;
+    });
+    let provider = this.tablesData[table].provider;
+    Services.obs.notifyObservers(null, "safebrowsing-update-begin", provider);
   }
 }
 
 /**
  * Callback function if the update request succeeded.
  * @param waitForUpdate String The number of seconds that the client should
  *        wait before requesting again.
  */
@@ -545,29 +558,34 @@ PROT_ListManager.prototype.updateSuccess
   log("Setting last update of " + provider + " to " + now);
   this.prefs_.setPref(lastUpdatePref, now.toString());
 
   let nextUpdatePref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
   let targetTime = now + delay;
   log("Setting next update of " + provider + " to " + targetTime
       + " (" + delay + "ms from now)");
   this.prefs_.setPref(nextUpdatePref, targetTime.toString());
+
+  Services.obs.notifyObservers(null, "safebrowsing-update-finished", "success");
 }
 
 /**
  * Callback function if the update request succeeded.
  * @param result String The error code of the failure
  */
 PROT_ListManager.prototype.updateError_ = function(table, updateUrl, result) {
   log("update error for " + table + " from " + updateUrl + ": " + result + "\n");
   // There was some trouble applying the updates. Don't try again for at least
   // updateInterval milliseconds.
   this.updateCheckers_[updateUrl] =
     new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
                 this.updateInterval, false);
+
+  Services.obs.notifyObservers(null, "safebrowsing-update-finished",
+                               "update error(" + result + ")");
 }
 
 /**
  * Callback function when the download failed
  * @param status String http status or an empty string if connection refused.
  */
 PROT_ListManager.prototype.downloadError_ = function(table, updateUrl, status) {
   log("download error for " + table + ": " + status + "\n");
@@ -584,16 +602,18 @@ PROT_ListManager.prototype.downloadError
     delay = this.requestBackoffs_[updateUrl].nextRequestDelay();
   } else {
     log("Got non error status for error callback?!");
   }
   this.updateCheckers_[updateUrl] =
     new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
                 delay, false);
 
+  Services.obs.notifyObservers(null, "safebrowsing-update-finished",
+                               "download error(" + status + ")");
 }
 
 PROT_ListManager.prototype.QueryInterface = function(iid) {
   if (iid.equals(Ci.nsISupports) ||
       iid.equals(Ci.nsIUrlListManager) ||
       iid.equals(Ci.nsITimerCallback))
     return this;
 
--- a/toolkit/components/url-classifier/nsIUrlListManager.idl
+++ b/toolkit/components/url-classifier/nsIUrlListManager.idl
@@ -22,16 +22,21 @@ interface nsIUrlListManagerCallback : ns
 interface nsIUrlListManager : nsISupports
 {
     /**
      * Get the gethash url for this table
      */
     ACString getGethashUrl(in ACString tableName);
 
     /**
+     * Get the update url for this table
+     */
+    ACString getUpdateUrl(in ACString tableName);
+
+    /**
      * Add a table to the list of tables we are managing. The name is a
      * string of the format provider_name-semantic_type-table_type.  For
      * @param tableName A string of the format
      *        provider_name-semantic_type-table_type.  For example,
      *        goog-white-enchash or goog-black-url.
      * @param providerName The name of the entity providing the list.
      * @param updateUrl The URL from which to fetch updates.
      * @param gethashUrl The URL from which to fetch hash completions.
@@ -59,9 +64,15 @@ interface nsIUrlListManager : nsISupport
 
     /**
      * Lookup a key.  Should not raise exceptions.  Calls the callback
      * function with a comma-separated list of tables to which the key
      * belongs.
      */
     void safeLookup(in nsIPrincipal key,
                     in nsIUrlListManagerCallback cb);
+
+    /**
+     * This is currently used by about:url-classifier to force an update
+     * for the update url. Update may still fail because of backoff algorithm.
+     */
+    boolean checkForUpdates(in ACString updateUrl);
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/content/aboutUrlClassifier.css
@@ -0,0 +1,72 @@
+/* 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/. */
+
+html {
+  --aboutUrlClassifier-table-background: #ebebeb;
+  background-color: var(--in-content-page-background);
+}
+
+body {
+  margin: 40px 48px;
+}
+
+.major-section {
+  margin-top: 2em;
+  margin-bottom: 1em;
+  font-size: large;
+  text-align: start;
+  font-weight: bold;
+}
+
+table {
+  background-color: var(--aboutUrlClassifier-table-background);
+  color: var(--in-content-text-color);
+  font: message-box;
+  text-align: start;
+  width: 100%;
+  border: 1px solid var(--in-content-border-color);
+  border-spacing: 0px;
+}
+
+th, td {
+  border: 1px solid var(--in-content-border-color);
+  padding: 4px;
+}
+
+thead th {
+  text-align: center;
+}
+
+th {
+  text-align: start;
+  background-color: var(--in-content-table-header-background);
+  color: var(--in-content-selected-text);
+}
+
+th.column {
+  white-space: nowrap;
+  width: 0px;
+}
+
+td {
+  text-align: start;
+  border-color: var(--in-content-table-border-dark-color);
+}
+
+#provider-table > tbody > tr >  td:last-child {
+  text-align: center;
+}
+
+#debug-table {
+  margin-top: 20px;
+}
+
+.options > .toggle-container-with-text {
+  display: inline-flex;
+}
+
+button {
+  margin-inline-start: 0;
+  margin-inline-end: 8px;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/content/aboutUrlClassifier.js
@@ -0,0 +1,335 @@
+/* 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/. */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const bundle = Services.strings.createBundle(
+  "chrome://global/locale/aboutUrlClassifier.properties");
+
+const UPDATE_BEGIN = "safebrowsing-update-begin";
+const UPDATE_FINISH = "safebrowsing-update-finished";
+const JSLOG_PREF = "browser.safebrowsing.debug";
+
+const STR_NA = bundle.GetStringFromName("NotAvailable");
+
+function unLoad() {
+  window.removeEventListener("unload", unLoad);
+
+  Provider.uninit();
+  Debug.uninit();
+}
+
+function onLoad() {
+  window.removeEventListener("load", onLoad);
+  window.addEventListener("unload", unLoad);
+
+  Provider.init();
+  Debug.init();
+}
+
+/*
+ * 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) {
+      this.providers.add(child.split(".")[0]);
+    }
+
+    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");
+    elem.childNodes[0].nodeValue = bundle.GetStringFromName("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");
+    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); });
+
+          let str = bundle.GetStringFromName("TriggerUpdate")
+          btn.appendChild(document.createTextNode(str));
+          td.appendChild(btn);
+	      } else {
+          let str = column.id === "col-lastupdateresult" ? STR_NA : "";
+          td.appendChild(document.createTextNode(str));
+        }
+        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) : STR_NA;
+
+      pref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+      let nut = Services.prefs.getCharPref(pref, "");
+      values["col-nextupdatetime"] = nut ? new Date(nut * 1) : STR_NA;
+
+      for (let key of Object.keys(values)) {
+        let elem = document.getElementById(provider + "-" + key);
+        elem.childNodes[0].nodeValue = values[key];
+      }
+    }
+  },
+
+  // 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 table = Services.prefs.getCharPref(pref, "").split(",")[0];
+
+    let updateUrl = listmanager.getUpdateUrl(table);
+    if (!listmanager.checkForUpdates(updateUrl)) {
+      // This may because of back-off algorithm.
+      let elem = document.getElementById(provider + "-col-lastupdateresult");
+      elem.childNodes[0].nodeValue = bundle.GetStringFromName("CannotUpdate");
+    }
+  },
+
+};
+
+/*
+ * Debug
+ */
+var Debug = {
+  // url-classifier NSPR Log modules.
+  modules: ["UrlClassifierDbService",
+            "nsChannelClassifier",
+            "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("div");
+      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 label = document.createElement("label");
+      label.for = chk.id;
+      label.appendChild(document.createTextNode(module));
+      container.appendChild(label, 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 env = Cc["@mozilla.org/process/environment;1"]
+              .getService(Ci.nsIEnvironment);
+
+    let logModules = env.get("MOZ_LOG") ||
+                     env.get("MOZ_LOG_MODULES") ||
+                     env.get("NSPR_LOG_MODULES");
+
+    if (logModules.length > 0) {
+      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 = env.get("MOZ_LOG_FILE") || env.get("NSPR_LOG_FILE");
+    if (logFile.length > 0) {
+      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");
+    curJSLog.childNodes[0].nodeValue = enabled ?
+      bundle.GetStringFromName("Enabled") :
+      bundle.GetStringFromName("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;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/content/aboutUrlClassifier.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD;
+<!ENTITY % urlClassifierDTD SYSTEM "chrome://global/locale/aboutUrlClassifier.dtd"> %urlClassifierDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://global/content/aboutUrlClassifier.css" type="text/css"/>
+  <script type="text/javascript" src="chrome://global/content/aboutUrlClassifier.js"></script>
+</head>
+
+<body onload="onLoad()" class="aboutPageWideContainer">
+  <h1>&aboutUrlClassifier.pageTitle;</h1>
+  <div id="provider">
+    <h2 class="major-section">&aboutUrlClassifier.providerTitle;</h2>
+    <table id="provider-table">
+      <thead>
+        <tr id="provider-head-row">
+          <th id="col-provider">&aboutUrlClassifier.provider;</th>
+          <th id="col-lastupdatetime">&aboutUrlClassifier.providerLastUpdateTime;</th>
+          <th id="col-nextupdatetime">&aboutUrlClassifier.providerNextUpdateTime;</th>
+          <th id="col-lastupdateresult">&aboutUrlClassifier.providerLastUpdateStatus;</th>
+          <th id="col-update">&aboutUrlClassifier.providerUpdateBtn;</th>
+        </tr>
+      </thead>
+      <tbody id="provider-table-body">
+        <!-- data is generated in javascript -->
+      </tbody>
+    </table>
+  </div>
+  <div id="debug">
+    <h2 class="major-section">&aboutUrlClassifier.debugTitle;</h2>
+    <div id="debug-modules" class="options">
+      <input id="log-modules" type="text" value=""/>
+      <button id="set-log-modules">&aboutUrlClassifier.debugModuleBtn;</button>
+      <br></br>
+      <input id="log-file" type="text" value=""/>
+      <button id="set-log-file">&aboutUrlClassifier.debugFileBtn;</button>
+      <br></br>
+      <div class="toggle-container-with-text">
+        <input id="js-log" type="checkbox"/>
+        <label for="js-log">&aboutUrlClassifier.debugJSLogChk;</label>
+      </div>
+    </div>
+    <table id="debug-table">
+    <tbody>
+    <tr>
+      <th class="column">&aboutUrlClassifier.debugSBModules;</th>
+      <td id="sb-log-modules">
+      </td>
+    </tr>
+    <tr>
+      <th class="column">&aboutUrlClassifier.debugModules;</th>
+      <td id="cur-log-modules">
+      </td>
+    </tr>
+    <tr>
+      <th class="column">&aboutUrlClassifier.debugSBJSModules;</th>
+      <td id="cur-js-log">
+      </td>
+    </tr>
+    <tr>
+      <th class="column">&aboutUrlClassifier.debugFile;</th>
+      <td id="cur-log-file">
+      </td>
+    </tr>
+    </tbody>
+    </table>
+  </div>
+</body>
+</html>
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -28,16 +28,19 @@ toolkit.jar:
    content/global/aboutwebrtc/aboutWebrtc.css   (aboutwebrtc/aboutWebrtc.css)
    content/global/aboutwebrtc/aboutWebrtc.js    (aboutwebrtc/aboutWebrtc.js)
    content/global/aboutwebrtc/aboutWebrtc.html (aboutwebrtc/aboutWebrtc.html)
    content/global/aboutSupport.js
 *  content/global/aboutSupport.xhtml
    content/global/aboutTelemetry.js
    content/global/aboutTelemetry.xhtml
    content/global/aboutTelemetry.css
+   content/global/aboutUrlClassifier.js
+   content/global/aboutUrlClassifier.xhtml
+   content/global/aboutUrlClassifier.css
    content/global/directionDetector.html
    content/global/plugins.html
    content/global/plugins.css
    content/global/browser-child.js
    content/global/browser-content.js
 *   content/global/buildconfig.html
    content/global/contentAreaUtils.js
 #ifndef MOZ_FENNEC
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/aboutUrlClassifier.dtd
@@ -0,0 +1,29 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE the term "url-classifier" should not be translated. -->
+<!ENTITY aboutUrlClassifier.pageTitle                   "Information about the url-classifier">
+<!ENTITY aboutUrlClassifier.providerTitle               "Provider">
+<!ENTITY aboutUrlClassifier.provider                    "Provider">
+<!ENTITY aboutUrlClassifier.providerLastUpdateTime      "Last update time">
+<!ENTITY aboutUrlClassifier.providerNextUpdateTime      "Next update time">
+<!ENTITY aboutUrlClassifier.providerLastUpdateStatus    "Last update status">
+<!ENTITY aboutUrlClassifier.providerUpdateBtn           "Update">
+<!ENTITY aboutUrlClassifier.lookupTitle                 "Lookup">
+<!ENTITY aboutUrlClassifier.lookupUrl                   "Url">
+<!ENTITY aboutUrlClassifier.lookupMatch                 "Match">
+<!ENTITY aboutUrlClassifier.lookupMatchBtn              "Check Match Result">
+<!ENTITY aboutUrlClassifier.lookupLookup                "Lookup">
+<!ENTITY aboutUrlClassifier.lookupBtn                   "Check Lookup Result">
+<!ENTITY aboutUrlClassifier.cacheTitle                  "Cache">
+<!ENTITY aboutUrlClassifier.memoryTitle                 "Memory">
+<!ENTITY aboutUrlClassifier.databaseTitle               "Database">
+<!ENTITY aboutUrlClassifier.debugTitle                  "Debug">
+<!ENTITY aboutUrlClassifier.debugModuleBtn              "Set Log Modules">
+<!ENTITY aboutUrlClassifier.debugFileBtn                "Set Log File">
+<!ENTITY aboutUrlClassifier.debugJSLogChk               "Set JS Log">
+<!ENTITY aboutUrlClassifier.debugSBModules              "Safe Browsing log modules">
+<!ENTITY aboutUrlClassifier.debugModules                "Current log modules">
+<!ENTITY aboutUrlClassifier.debugSBJSModules            "Safe Browsing JS log">
+<!ENTITY aboutUrlClassifier.debugFile                   "Current log file">
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/aboutUrlClassifier.properties
@@ -0,0 +1,19 @@
+# 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/.
+
+TriggerUpdate = Trigger Update
+
+NotAvailable = N/A
+
+DisableSBJSLog = Disable Safe Browsing JS Log
+
+EnableSBJSLog = Enable Safe Browsing JS Log
+
+Enabled = Enabled
+
+Disabled = Disabled
+
+Updating = updating
+
+CannotUpdate = cannot update
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -16,16 +16,18 @@
   locale/@AB_CD@/global/aboutProfiles.properties        (%chrome/global/aboutProfiles.properties)
 #endif
   locale/@AB_CD@/global/aboutServiceWorkers.dtd         (%chrome/global/aboutServiceWorkers.dtd)
   locale/@AB_CD@/global/aboutServiceWorkers.properties  (%chrome/global/aboutServiceWorkers.properties)
   locale/@AB_CD@/global/aboutSupport.dtd                (%chrome/global/aboutSupport.dtd)
   locale/@AB_CD@/global/aboutSupport.properties         (%chrome/global/aboutSupport.properties)
   locale/@AB_CD@/global/aboutTelemetry.dtd              (%chrome/global/aboutTelemetry.dtd)
   locale/@AB_CD@/global/aboutTelemetry.properties       (%chrome/global/aboutTelemetry.properties)
+  locale/@AB_CD@/global/aboutUrlClassifier.dtd          (%chrome/global/aboutUrlClassifier.dtd)
+  locale/@AB_CD@/global/aboutUrlClassifier.properties   (%chrome/global/aboutUrlClassifier.properties)
   locale/@AB_CD@/global/aboutWebrtc.properties          (%chrome/global/aboutWebrtc.properties)
   locale/@AB_CD@/global/autocomplete.properties         (%chrome/global/autocomplete.properties)
   locale/@AB_CD@/global/appPicker.dtd                   (%chrome/global/appPicker.dtd)
   locale/@AB_CD@/global/brand.dtd                       (generic/chrome/global/brand.dtd)
   locale/@AB_CD@/global/browser.properties              (%chrome/global/browser.properties)
   locale/@AB_CD@/global/charsetMenu.dtd                 (%chrome/global/charsetMenu.dtd)
   locale/@AB_CD@/global/charsetMenu.properties          (%chrome/global/charsetMenu.properties)
   locale/@AB_CD@/global/commonDialog.dtd                (%chrome/global/commonDialog.dtd)