Bug 1488845 - Add an about:compat page to the Webcompat GoFaster addon. r=denschub,kmag,Pike
authorThomas Wisniewski <twisniewski@mozilla.com>
Mon, 25 Feb 2019 23:10:41 +0000
changeset 518892 ecb22648747e03be200af692c6c02c3eb0e2dac3
parent 518891 46ca7bb91c8e3315a7caaa0a73eb32a7c0b604a4
child 518893 575da58a58c7d38702bca83a2e45d95b075d17da
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdenschub, kmag, Pike
bugs1488845
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1488845 - Add an about:compat page to the Webcompat GoFaster addon. r=denschub,kmag,Pike Add an about:compat page to the Webcompat GoFaster addon. Differential Revision: https://phabricator.services.mozilla.com/D18303
browser/base/content/test/performance/browser_startup_content.js
browser/base/content/test/static/browser_all_files_referenced.js
browser/extensions/webcompat/AboutCompat.jsm
browser/extensions/webcompat/aboutCompat.css
browser/extensions/webcompat/aboutCompat.html
browser/extensions/webcompat/aboutCompat.js
browser/extensions/webcompat/aboutConfigPrefs.js
browser/extensions/webcompat/aboutPage.js
browser/extensions/webcompat/aboutPage.json
browser/extensions/webcompat/aboutPageProcessScript.js
browser/extensions/webcompat/background.js
browser/extensions/webcompat/injections.js
browser/extensions/webcompat/jar.mn
browser/extensions/webcompat/manifest.json
browser/extensions/webcompat/moz.build
browser/extensions/webcompat/ua_overrides.js
mobile/android/extensions/webcompat/AboutCompat.jsm
mobile/android/extensions/webcompat/aboutCompat.css
mobile/android/extensions/webcompat/aboutCompat.html
mobile/android/extensions/webcompat/aboutCompat.js
mobile/android/extensions/webcompat/aboutConfigPrefs.js
mobile/android/extensions/webcompat/aboutPage.js
mobile/android/extensions/webcompat/aboutPage.json
mobile/android/extensions/webcompat/aboutPageProcessScript.js
mobile/android/extensions/webcompat/background.js
mobile/android/extensions/webcompat/injections.js
mobile/android/extensions/webcompat/jar.mn
mobile/android/extensions/webcompat/manifest.json
mobile/android/extensions/webcompat/moz.build
mobile/android/extensions/webcompat/ua_overrides.js
mobile/android/locales/filter.py
mobile/android/locales/jar.mn
mobile/android/locales/l10n.toml
mobile/locales/filter.py
toolkit/locales/en-US/toolkit/about/aboutCompat.ftl
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -81,26 +81,30 @@ const whitelist = {
     "resource://gre/modules/addons/Content.js",
   ]),
   processScripts: new Set([
     "chrome://global/content/process-content.js",
     "resource:///modules/ContentObservers.js",
     "data:,ChromeUtils.import('resource://gre/modules/ExtensionProcessScript.jsm')",
     "resource://devtools/client/jsonview/converter-observer.js",
     "resource://gre/modules/WebRequestContent.js",
+    "resource://webcompat/aboutPageProcessScript.js",
   ]),
 };
 
 // Items on this list are allowed to be loaded but not
 // required, as opposed to items in the main whitelist,
 // which are all required.
 const intermittently_loaded_whitelist = {
   modules: new Set([
     "resource://gre/modules/nsAsyncShutdown.jsm",
     "resource://gre/modules/sessionstore/Utils.jsm",
+
+    // Webcompat about:config front-end
+    "resource://webcompat/AboutCompat.jsm",
   ]),
   frameScripts: new Set([]),
   processScripts: new Set([]),
 };
 
 const blacklist = {
   services: new Set([
     "@mozilla.org/base/telemetry-startup;1",
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -176,16 +176,18 @@ var whitelist = [
   // kvstore.jsm wraps the API in nsIKeyValue.idl in a more ergonomic API
   // It landed in bug 1490496, and we expect to start using it shortly.
   {file: "resource://gre/modules/kvstore.jsm"},
   {file: "chrome://devtools/content/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.ftl",
    isFromDevTools: true},
   // Bug 1526672
   {file: "resource://app/localization/en-US/browser/touchbar/touchbar.ftl",
    platforms: ["linux", "win"]},
+  // Referenced by the webcompat system addon for localization
+  {file: "resource://gre/localization/en-US/toolkit/about/aboutCompat.ftl"},
 ];
 
 whitelist = new Set(whitelist.filter(item =>
   ("isFromDevTools" in item) == isDevtools &&
   (!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
   (!item.platforms || item.platforms.includes(AppConstants.platform))
 ).map(item => item.file));
 
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/AboutCompat.jsm
@@ -0,0 +1,35 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["AboutCompat"];
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const addonID = "webcompat@mozilla.org";
+const addonPageRelativeURL = "/aboutCompat.html";
+
+function AboutCompat() {
+  this.chromeURL = WebExtensionPolicy.getByID(addonID).getURL(addonPageRelativeURL);
+}
+AboutCompat.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIAboutModule]),
+  getURIFlags() {
+    return Ci.nsIAboutModule.ALLOW_SCRIPT |
+           Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD;
+  },
+
+  newChannel(aURI, aLoadInfo) {
+    const uri = Services.io.newURI(this.chromeURL);
+    aLoadInfo.resultPrincipalURI = uri;
+    const channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+    channel.originalURI = aURI;
+
+    if (this.uriFlags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) {
+      channel.owner = null;
+    }
+    return channel;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutCompat.css
@@ -0,0 +1,190 @@
+@media screen and (min-device-width:481px) {
+  :root {
+    font-family: sans-serif;
+    margin: 40px auto;
+    min-width: 30em;
+    max-width: 60em;
+  }
+
+  table {
+    width: 100%;
+    padding-bottom: 2em;
+  }
+
+  .float-right {
+    float: right;
+  }
+
+  .hidden {
+    display: none;
+  }
+
+  .table-title-container {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .wide-button {
+    display: block;
+    min-height: 32px;
+    padding-left: 30px;
+    padding-right: 30px;
+  }
+
+  .submitting {
+    background-image: url(chrome://global/skin/icons/loading.png);
+    background-position: center;
+    background-repeat: no-repeat;
+    background-size: 16px;
+  }
+
+  .submitting .submit-crash-button-label {
+    display: none;
+  }
+
+  .failed-to-submit {
+    color: #ca8695;
+  }
+
+  a.button-as-link {
+    -moz-appearance: none;
+    min-height: 30px;
+    color: var(--in-content-text-color) !important;
+    border: 1px solid var(--in-content-box-border-color) !important;
+    border-radius: 2px;
+    background-color: var(--in-content-page-background);
+    line-height: 30px;
+    margin: 4px 8px;
+    /* Ensure font-size isn't overridden by widget styling (e.g. in forms.css) */
+    font-size: 1em;
+  }
+
+  a.button-as-link:hover {
+    background-color: var(--in-content-box-background-hover) !important;
+    text-decoration: none;
+  }
+
+  h2.lighter-font-weight {
+    font-weight: lighter;
+  }
+
+  html[dir=ltr] th {
+    text-align: left;
+  }
+
+  html[dir=rtl] th {
+    text-align: right;
+  }
+}
+
+@media screen and (min-device-width:320px) and (max-device-width:480px) {
+  * {
+    margin: 0;
+    padding: 0;
+  }
+
+  html {
+    font-family: sans-serif;
+    font-size: 14px;
+    -moz-text-size-adjust: none;
+    background-color: #f5f5f5;
+  }
+
+  table, tr, p {
+    display: block;
+    background: #fff;
+  }
+
+  table {
+    border-top: 2px solid #0a84ff;
+    margin-top: -2px;
+    position: absolute;
+    width: 100%;
+    z-index: 1;
+    display: none;
+  }
+
+  tr {
+    position: relative;
+    border-bottom: 1px solid #d7d9db;
+    padding: 1em;
+  }
+
+  a {
+    color: #000;
+    font-size: 94%;
+  }
+
+  .tab {
+    cursor: pointer;
+    position: relative;
+    z-index: 2;
+    display: inline-block;
+    text-align: left;
+    padding: 1em;
+    font-weight: bold;
+    border-top-left-radius: 3px;
+    border-top-right-radius: 3px;
+    border: 1px solid #d7d9db;
+    border-bottom: 0;
+    margin-bottom: 2px;
+    background: #f5f5f5;
+    color: #363b40;
+    font-size: 1em;
+    font-weight: bold;
+    padding: 1em;
+  }
+
+  .tab.active {
+    border-bottom-color: #fff;
+    background: #fff;
+    margin-bottom: 0;
+    padding-bottom: calc(1em + 2px);
+  }
+
+  .tab.active + table {
+    display: block;
+  }
+
+  td {
+    display: block;
+    position: relative;
+    padding-right: 6.5em;
+  }
+
+  td[colspan="4"] {
+    padding: 1em;
+    font-style: italic;
+    text-align: center;
+  }
+
+  td:not([colspan]):nth-child(1) {
+    font-weight: bold;
+  }
+
+  td:not([colspan]):nth-child(1) {
+    padding-bottom: 0.25em;
+  }
+
+  td:nth-child(3) {
+    display: contents;
+  }
+
+  button {
+    background: #e8e8e7;
+    position: absolute;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    width: 6em;
+    border: 0;
+    border-left: 1px solid #d7d9db;
+    -moz-appearance: none;
+    color: #000;
+  }
+
+  button::-moz-focus-inner {
+    border: 0;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutCompat.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="stylesheet" href="aboutCompat.css" />
+  <link rel="stylesheet" media="screen and (min-device-width:481px), projection" type="text/css"
+          href="chrome://global/skin/in-content/common.css"/>
+  <link rel="localization" href="toolkit/about/aboutCompat.ftl"/>
+  <title data-l10n-id="text-title"></title>
+  <script src="aboutCompat.js"></script>
+  </head>
+<body>
+  <h2 class="tab active" data-l10n-id="label-overrides"></h2>
+  <table id="overrides">
+    <col/>
+    <col/>
+    <col/>
+  </table>
+  <h2 class="tab" data-l10n-id="label-interventions"></h2>
+  <table id="interventions">
+    <col/>
+    <col/>
+    <col/>
+  </table>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutCompat.js
@@ -0,0 +1,146 @@
+/* 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";
+
+/* globals browser */
+
+const portToAddon = (function() {
+  let port;
+
+  function connect() {
+    port = browser.runtime.connect({name: "AboutCompatTab"});
+    port.onMessage.addListener(onMessageFromAddon);
+    port.onDisconnect.addListener(e => {
+      port = undefined;
+    });
+  }
+
+  connect();
+
+  async function send(message) {
+    if (port) {
+      return port.postMessage(message);
+    }
+    return Promise.reject("background script port disconnected");
+  }
+
+  return {send};
+}());
+
+const $ = function(sel) { return document.querySelector(sel); };
+
+const DOMContentLoadedPromise = new Promise(resolve => {
+  document.addEventListener("DOMContentLoaded", () => {
+    resolve();
+  }, {once: true});
+});
+
+Promise.all([
+  browser.runtime.sendMessage("getOverridesAndInterventions"),
+  DOMContentLoadedPromise,
+]).then(([info]) => {
+  document.body.addEventListener("click", async evt => {
+    const ele = evt.target;
+    if (ele.nodeName === "BUTTON") {
+      const row = ele.closest("[data-id]");
+      if (row) {
+        evt.preventDefault();
+        ele.disabled = true;
+        const id = row.getAttribute("data-id");
+        try {
+          await browser.runtime.sendMessage({command: "toggle", id});
+        } catch (_) {
+          ele.disabled = false;
+        }
+      }
+    } else if (ele.classList.contains("tab")) {
+      document.querySelectorAll(".tab").forEach(tab => {
+        tab.classList.remove("active");
+      });
+      ele.classList.add("active");
+    }
+  });
+
+  redraw(info);
+});
+
+function onMessageFromAddon(msg) {
+  if ("interventionsChanged" in msg) {
+    redrawTable($("#interventions"), msg.interventionsChanged);
+  }
+
+  if ("overridesChanged" in msg) {
+    redrawTable($("#overrides"), msg.overridesChanged);
+  }
+
+  const id = msg.toggling || msg.toggled;
+  const button = $(`[data-id="${id}"] button`);
+  if (!button) {
+    return;
+  }
+  const active = msg.active;
+  document.l10n.setAttributes(button,
+                      active ? "label-disable" : "label-enable");
+  button.disabled = !!msg.toggling;
+}
+
+function redraw(info) {
+  const {overrides, interventions} = info;
+  redrawTable($("#overrides"), overrides);
+  redrawTable($("#interventions"), interventions);
+}
+
+function redrawTable(table, data) {
+  const df = document.createDocumentFragment();
+  table.querySelectorAll("tr").forEach(tr => { tr.remove(); });
+
+  let noEntriesMessage;
+  if (data === false) {
+    noEntriesMessage = "text-disabled-in-about-config";
+  } else if (data.length === 0) {
+    noEntriesMessage = table.id === "overrides" ? "text-no-overrides"
+                                                : "text-no-interventions";
+  }
+
+  if (noEntriesMessage) {
+    const tr = document.createElement("tr");
+    df.appendChild(tr);
+
+    const td = document.createElement("td");
+    td.setAttribute("colspan", "3");
+    document.l10n.setAttributes(td, noEntriesMessage);
+    tr.appendChild(td);
+
+    table.appendChild(df);
+    return;
+  }
+
+  for (const row of data) {
+    const tr = document.createElement("tr");
+    tr.setAttribute("data-id", row.id);
+    df.appendChild(tr);
+
+    let td = document.createElement("td");
+    td.innerText = row.domain;
+    tr.appendChild(td);
+
+    td = document.createElement("td");
+    const a = document.createElement("a");
+    const bug = row.bug;
+    a.href = `https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`;
+    document.l10n.setAttributes(a, "label-more-information", {bug});
+    a.target = "aboutCompatBug";
+    td.appendChild(a);
+    tr.appendChild(td);
+
+    td = document.createElement("td");
+    tr.appendChild(td);
+    const button = document.createElement("button");
+    document.l10n.setAttributes(button,
+                        row.active ? "label-disable" : "label-enable");
+    td.appendChild(button);
+  }
+  table.appendChild(df);
+}
--- a/browser/extensions/webcompat/aboutConfigPrefs.js
+++ b/browser/extensions/webcompat/aboutConfigPrefs.js
@@ -1,17 +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/. */
 
 "use strict";
 
-/* global ExtensionAPI, ExtensionCommon */
+/* global ExtensionAPI, ExtensionCommon, Services, XPCOMUtils */
 
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
 
 this.aboutConfigPrefs = class extends ExtensionAPI {
   getAPI(context) {
     const EventManager = ExtensionCommon.EventManager;
     const extensionIDBase = context.extension.id.split("@")[0];
     const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
 
     return {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutPage.js
@@ -0,0 +1,47 @@
+/* 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";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+ChromeUtils.defineModuleGetter(this, "AppConstants",
+                               "resource://gre/modules/AppConstants.jsm");
+
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "resProto",
+                                   "@mozilla.org/network/protocol;1?name=resource",
+                                   "nsISubstitutingProtocolHandler");
+
+const ResourceSubstitution = "webcompat";
+const ProcessScriptURL = "resource://webcompat/aboutPageProcessScript.js";
+
+const ShouldStart = ["default", "nightly", "nightly-try"].includes(AppConstants.MOZ_UPDATE_CHANNEL);
+
+this.aboutPage = class extends ExtensionAPI {
+  onStartup() {
+    if (!ShouldStart) {
+      return;
+    }
+
+    const {rootURI} = this.extension;
+
+    resProto.setSubstitution(ResourceSubstitution,
+                             Services.io.newURI("chrome/res/", null, rootURI));
+
+    Services.ppmm.loadProcessScript(ProcessScriptURL, true);
+  }
+
+  onShutdown() {
+    if (!ShouldStart) {
+      return;
+    }
+
+    resProto.setSubstitution(ResourceSubstitution, null);
+
+    Services.ppmm.removeDelayedProcessScript(ProcessScriptURL);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutPage.json
@@ -0,0 +1,4 @@
+[{
+    "namespace": "aboutCompat",
+    "description": "Enables the about:compat page"
+}]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/aboutPageProcessScript.js
@@ -0,0 +1,22 @@
+/* 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";
+
+const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+const classID = Components.ID("{97bf9550-2a7b-11e9-b56e-0800200c9a66}");
+
+if (!Cm.isCIDRegistered(classID)) {
+  const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+  const factory = XPCOMUtils.generateSingletonFactory(function() {
+    const {AboutCompat} = ChromeUtils.import("resource://webcompat/AboutCompat.jsm");
+    return new AboutCompat();
+  });
+
+  Cm.registerFactory(classID, "about:compat",
+                     "@mozilla.org/network/protocol/about;1?what=compat",
+                     factory);
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/background.js
@@ -0,0 +1,92 @@
+/* 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";
+
+/* global browser, disableInjection, disableOverride, enableInjection,
+          enableOverride, InjectionsEnabled, UAOverridesEnabled */
+
+const UAOverrides = [];
+const Injections = [];
+
+function getOverrideOrInterventionById(id) {
+  for (const [type, things] of Object.entries({
+    overrides: UAOverrides,
+    interventions: Injections,
+  })) {
+    for (const what of things) {
+      if (what.id === id) {
+        return {type, what};
+      }
+    }
+  }
+  return {};
+}
+
+const portsToAboutCompatTabs = (function() {
+  const ports = new Set();
+
+  browser.runtime.onConnect.addListener(port => {
+    ports.add(port);
+    port.onDisconnect.addListener(function() {
+      ports.delete(port);
+    });
+  });
+
+  async function broadcast(message) {
+    for (const port of ports) {
+      port.postMessage(message);
+    }
+  }
+
+  return {broadcast};
+}());
+
+function filterOverrides(overrides) {
+  return overrides.filter(override => override.availableOnPlatform).map(override => {
+    const {id, active, bug, domain} = override;
+    return {id, active, bug, domain};
+  });
+}
+
+browser.runtime.onMessage.addListener(msg => {
+  switch (msg.command || msg) {
+    case "toggle": {
+      const id = msg.id;
+      const {type, what} = getOverrideOrInterventionById(id);
+      if (!what) {
+        return Promise.reject(`No such override or intervention to toggle: ${id}`);
+      }
+      portsToAboutCompatTabs.broadcast({toggling: id, active: what.active}).then(async () => {
+        switch (type) {
+          case "interventions": {
+            if (what.active) {
+              await disableInjection(what);
+            } else {
+              await enableInjection(what);
+            }
+            break;
+          }
+          case "overrides": {
+            if (what.active) {
+              await disableOverride(what);
+            } else {
+              await enableOverride(what);
+            }
+            break;
+          }
+        }
+        portsToAboutCompatTabs.broadcast({toggled: id, active: what.active});
+      });
+      break;
+    }
+    case "getOverridesAndInterventions": {
+      return Promise.resolve({
+        overrides: UAOverridesEnabled && filterOverrides(UAOverrides) || false,
+        interventions: InjectionsEnabled && filterOverrides(Injections) || false,
+      });
+    }
+  }
+  return undefined;
+});
--- a/browser/extensions/webcompat/injections.js
+++ b/browser/extensions/webcompat/injections.js
@@ -1,82 +1,140 @@
 /**
  * For detailed information on our policies, and a documention on this format
  * and its possibilites, please check the Mozilla-Wiki at
  *
  * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
  */
-const contentScripts = {
-  universal: [
-    {
+
+"use strict";
+
+/* globals browser, filterOverrides, Injections, portsToAboutCompatTabs */
+
+let InjectionsEnabled = true;
+
+for (const injection of [
+  {
+    id: "testinjection",
+    platform: "all",
+    domain: "webcompat-addon-testcases.schub.io",
+    bug: "1287966",
+    contentScripts: {
       matches: ["*://webcompat-addon-testcases.schub.io/*"],
       css: [{file: "injections/css/bug0000000-dummy-css-injection.css"}],
       js: [{file: "injections/js/bug0000000-dummy-js-injection.js"}],
       runAt: "document_start",
     },
-  ],
-  desktop: [
-    {
+  }, {
+    id: "bug1452707",
+    platform: "desktop",
+    domain: "ib.absa.co.za",
+    bug: "1452707",
+    contentScripts: {
       matches: ["https://ib.absa.co.za/*"],
       js: [{file: "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js"}],
       runAt: "document_start",
     },
-    {
+  }, {
+    id: "bug1457335",
+    platform: "desktop",
+    domain: "histography.io",
+    bug: "1457335",
+    contentScripts: {
       matches: ["*://histography.io/*"],
       js: [{file: "injections/js/bug1457335-histography.io-ua-change.js"}],
       runAt: "document_start",
     },
-    {
+  }, {
+    id: "bug1472075",
+    platform: "desktop",
+    domain: "bankofamerica.com",
+    bug: "1472075",
+    contentScripts: {
       matches: ["*://*.bankofamerica.com/*"],
       js: [{file: "injections/js/bug1472075-bankofamerica.com-ua-change.js"}],
       runAt: "document_start",
     },
-    {
+  }, {
+    id: "bug1472081",
+    platform: "desktop",
+    domain: "election.gov.np",
+    bug: "1472081",
+    contentScripts: {
       matches: ["http://202.166.205.141/bbvrs/*"],
       js: [{file: "injections/js/bug1472081-election.gov.np-window.sidebar-shim.js"}],
       runAt: "document_start",
       allFrames: true,
     },
-    {
+  }, {
+    id: "bug1482066",
+    platform: "desktop",
+    domain: "portalminasnet.com",
+    bug: "1482066",
+    contentScripts: {
       matches: ["*://portalminasnet.com/*"],
       js: [{file: "injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js"}],
       runAt: "document_start",
       allFrames: true,
     },
-  ],
-  android: [],
-};
-
-/* globals browser */
+  },
+]) {
+  Injections.push(injection);
+}
 
 let port = browser.runtime.connect();
-let registeredContentScripts = [];
+const ActiveInjections = new Map();
 
 async function registerContentScripts() {
-  let platform = "desktop";
+  const platformMatches = ["all"];
   let platformInfo = await browser.runtime.getPlatformInfo();
-  if (platformInfo.os == "android") {
-    platform = "android";
+  platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
+
+  for (const injection of Injections) {
+    if (platformMatches.includes(injection.platform)) {
+      injection.availableOnPlatform = true;
+      await enableInjection(injection);
+    }
   }
 
-  let targetContentScripts = contentScripts.universal.concat(contentScripts[platform]);
-  targetContentScripts.forEach(async (contentScript) => {
-    try {
-      let handle = await browser.contentScripts.register(contentScript);
-      registeredContentScripts.push(handle);
-    } catch (ex) {
-      console.error("Registering WebCompat GoFaster content scripts failed: ", ex);
-    }
-  });
+  InjectionsEnabled = true;
+  portsToAboutCompatTabs.broadcast({interventionsChanged: filterOverrides(Injections)});
+}
+
+async function enableInjection(injection) {
+  if (injection.active) {
+    return;
+  }
+
+  try {
+    const handle = await browser.contentScripts.register(injection.contentScripts);
+    ActiveInjections.set(injection, handle);
+    injection.active = true;
+  } catch (ex) {
+    console.error("Registering WebCompat GoFaster content scripts failed: ", ex);
+  }
 }
 
 function unregisterContentScripts() {
-  registeredContentScripts.forEach((contentScript) => {
-    contentScript.unregister();
-  });
+  for (const injection of Injections) {
+    disableInjection(injection);
+  }
+  InjectionsEnabled = false;
+  portsToAboutCompatTabs.broadcast({interventionsChanged: false});
+}
+
+async function disableInjection(injection) {
+  if (!injection.active) {
+    return;
+  }
+
+  const contentScript = ActiveInjections.get(injection);
+  await contentScript.unregister();
+  ActiveInjections.delete(injection);
+  injection.active = false;
 }
 
 port.onMessage.addListener((message) => {
   switch (message.type) {
     case "injection-pref-changed":
       if (message.prefState) {
         registerContentScripts();
       } else {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+[features/webcompat@mozilla.org] chrome.jar:
+  res/AboutCompat.jsm (AboutCompat.jsm)
+  res/aboutPageProcessScript.js (aboutPageProcessScript.js)
--- a/browser/extensions/webcompat/manifest.json
+++ b/browser/extensions/webcompat/manifest.json
@@ -1,37 +1,46 @@
 {
   "manifest_version": 2,
   "name": "Web Compat",
   "description": "Urgent post-release fixes for web compatibility.",
-  "version": "3.0.0",
+  "version": "4.0.0",
 
   "applications": {
     "gecko": {
       "id": "webcompat@mozilla.org",
       "strict_min_version": "59.0b5"
     }
   },
 
   "experiment_apis": {
     "aboutConfigPrefs": {
       "schema": "aboutConfigPrefs.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "aboutConfigPrefs.js",
         "paths": [["aboutConfigPrefs"]]
       }
+    },
+    "aboutPage": {
+      "schema": "aboutPage.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "aboutPage.js",
+        "events": ["startup"]
+      }
     }
   },
 
   "permissions": [
     "webRequest",
     "webRequestBlocking",
     "<all_urls>"
   ],
 
   "background": {
     "scripts": [
+      "background.js",
       "injections.js",
       "ua_overrides.js"
     ]
   }
 }
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -3,18 +3,24 @@
 # 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/.
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org'] += [
+  'aboutCompat.css',
+  'aboutCompat.html',
+  'aboutCompat.js',
   'aboutConfigPrefs.js',
   'aboutConfigPrefs.json',
+  'aboutPage.js',
+  'aboutPage.json',
+  'background.js',
   'injections.js',
   'manifest.json',
   'ua_overrides.js'
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['css'] += [
   'injections/css/bug0000000-dummy-css-injection.css'
 ]
@@ -23,10 +29,12 @@ FINAL_TARGET_FILES.features['webcompat@m
   'injections/js/bug0000000-dummy-js-injection.js',
   'injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js',
   'injections/js/bug1457335-histography.io-ua-change.js',
   'injections/js/bug1472075-bankofamerica.com-ua-change.js',
   'injections/js/bug1472081-election.gov.np-window.sidebar-shim.js',
   'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js'
 ]
 
+JAR_MANIFESTS += ['jar.mn']
+
 with Files('**'):
   BUG_COMPONENT = ('Web Compatibility Tools', 'Go Faster')
--- a/browser/extensions/webcompat/ua_overrides.js
+++ b/browser/extensions/webcompat/ua_overrides.js
@@ -1,326 +1,417 @@
 /**
  * For detailed information on our policies, and a documention on this format
  * and its possibilites, please check the Mozilla-Wiki at
  *
  * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
  */
-const UAOverrides = {
-  universal: [
-    /*
-     * This is a dummy override that applies a Chrome UA to a dummy site that
-     * blocks all browsers but Chrome.
-     *
-     * This was only put in place to allow QA to test this system addon on an
-     * actual site, since we were not able to find a proper override in time.
-     */
-    {
+
+"use strict";
+
+/* globals filterOverrides, portsToAboutCompatTabs, UAOverrides */
+
+let UAOverridesEnabled = true;
+
+for (const override of [
+  {
+    id: "testoverride",
+    platform: "all",
+    domain: "webcompat-addon-testcases.schub.io",
+    bug: "1287966",
+    config: {
       matches: ["*://webcompat-addon-testcases.schub.io/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36";
       },
     },
-  ],
-  desktop: [
+  }, {
     /*
      * Bug 1464106 - directvnow.com - Create a UA override for Directvnow.com for playback on desktop
      * WebCompat issue #3846 - https://webcompat.com/issues/3846
      *
      * directvnow.com is blocking Firefox via UA sniffing. Outreach is still going
      * on, and playback works fine if we spoof as Chrome.
      */
-    {
+    id: "bug1464106",
+    platform: "desktop",
+    domain: "directvnow.com",
+    bug: "1464106",
+    config: {
       matches: ["*://*.directvnow.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36";
       },
     },
-  ],
-  android: [
+  }, {
     /*
      * Bug 1480710 - m.imgur.com - Build UA override
      * WebCompat issue #13154 - https://webcompat.com/issues/13154
      *
      * imgur returns a 404 for requests to CSS and JS file if requested with a Fennec
      * User Agent. By removing the Fennec identifies and adding Chrome Mobile's, we
      * receive the correct CSS and JS files.
      */
-    {
+    id: "bug1480710",
+    platform: "android",
+    domain: "m.imgur.com",
+    bug: "1480710",
+    config: {
       matches: ["*://m.imgur.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 755590 - sites.google.com - top bar doesn't show up in Firefox for Android
      *
      * Google Sites does show a different top bar template based on the User Agent.
      * For Fennec, this results in a broken top bar. Appending Chrome and Mobile Safari
      * identifiers to the UA results in a correct rendering.
      */
-    {
+    id: "bug755590",
+    platform: "android",
+    domain: "sites.google.com",
+    bug: "755590",
+    config: {
       matches: ["*://sites.google.com/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " Chrome/68.0.3440.85 Mobile Safari/537.366";
       },
     },
-
+  }, {
     /*
      * Bug 945963 - tieba.baidu.com serves simplified mobile content to Firefox Android
      * WebCompat issue #18455 - https://webcompat.com/issues/18455
      *
      * tieba.baidu.com and tiebac.baidu.com serve a heavily simplified and less functional
      * mobile experience to Firefox for Android users. Adding the AppleWebKit indicator
      * to the User Agent gets us the same experience.
      */
-    {
+    id: "bug945963",
+    platform: "android",
+    domain: "tieba.baidu.com",
+    bug: "945963",
+    config: {
       matches: ["*://tieba.baidu.com/*", "*://tiebac.baidu.com/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " AppleWebKit/537.36 (KHTML, like Gecko)";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "weather.yahoo.co.jp",
+    bug: "1177298",
+    config: {
       matches: ["*://weather.yahoo.co.jp/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "lohaco.jp",
+    bug: "1177298",
+    config: {
       matches: ["*://*.lohaco.jp/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "nhk.or.jp",
+    bug: "1177298",
+    config: {
       matches: ["*://*.nhk.or.jp/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " AppleWebKit";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "uniqlo.com",
+    bug: "1177298",
+    config: {
       matches: ["*://*.uniqlo.com/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " Mobile Safari";
       },
     },
-
+  }, {
     /*
      * Bug 1338260 - Add UA override for directTV
      * (Imported from ua-update.json.in)
      *
      * DirectTV has issues with scrolling and cut-off images. Pretending to be
      * Chrome for Android fixes those issues.
      */
-    {
+    id: "bug1338260",
+    platform: "android",
+    domain: "directv.com",
+    bug: "1338260",
+    config: {
       matches: ["*://*.directv.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1385206 - Create UA override for rakuten.co.jp on Firefox Android
      * (Imported from ua-update.json.in)
      *
      * rakuten.co.jp serves a Desktop version if Firefox is included in the UA.
      */
-    {
+    id: "bug1385206",
+    platform: "android",
+    domain: "rakuten.co.jp",
+    bug: "1385206",
+    config: {
       matches: ["*://*.rakuten.co.jp/*"],
       uaTransformer: (originalUA) => {
         return originalUA.replace(/Firefox.+$/, "");
       },
     },
-
+  }, {
     /*
      * Bug 969844 - mobile.de sends desktop site to Firefox on Android
      *
      * mobile.de sends the desktop site to Fennec. Spooing as Chrome works fine.
      */
-    {
+    id: "bug969844",
+    platform: "android",
+    domain: "mobile.de",
+    bug: "969844",
+    config: {
       matches: ["*://*.mobile.de/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1476436 - mobile.bet365.com - add UA override for fennec
      * WebCompat issue #17010 - https://webcompat.com/issues/17010
      *
      * mobile.bet365.com serves fennec an alternative version with less interactive
      * elements, although they work just fine. Spoofing as Chrome makes the
      * interactive elements appear.
      */
-    {
+    id: "bug1476436",
+    platform: "android",
+    domain: "mobile.bet365.com",
+    bug: "1476436",
+    config: {
       matches: ["*://mobile.bet365.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1509831 - cc.com - Add UA override for CC.com
      * WebCompat issue #329 - https://webcompat.com/issues/329
      *
      * ComedyCentral blocks Firefox for not being able to play HLS, which was
      * true in previous versions, but no longer is. With a spoofed Chrome UA,
      * the site works just fine.
      */
-    {
+    id: "bug1509831",
+    platform: "android",
+    domain: "cc.com",
+    bug: "1509831",
+    config: {
       matches: ["*://*.cc.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1508564 - cnbc.com - Add UA override for videos on www.cnbc.com
      * WebCompat issue #8410 - https://webcompat.com/issues/8410
      *
      * The video framework loaded in via pdk.theplatform.com fails to
      * acknowledge that Firefox does support HLS, so it fails to find a
      * supported video format and shows the loading bar forever. Spoofing as
      * Chrome works.
      */
-    {
+    id: "bug1508564",
+    platform: "android",
+    domain: "cnbc.com",
+    bug: "1508564",
+    config: {
       matches: ["*://*.cnbc.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1508516 - cineflix.com.br - Add UA override for cineflix.com.br/m/
      * WebCompat issue #21553 - https://webcompat.com/issues/21553
      *
      * The site renders a blank page with any Firefox snipped in the UA as it
      * is running into an exception. Spoofing as Chrome makes the site work
      * fine.
      */
-    {
+    id: "bug1508516",
+    platform: "android",
+    domain: "cineflix.com.br",
+    bug: "1508516",
+    config: {
       matches: ["*://*.cineflix.com.br/m/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1509852 - redbull.com - Add UA override for redbull.com
      * WebCompat issue #21439 - https://webcompat.com/issues/21439
      *
      * Redbull.com blocks some features, for example the live video player, for
      * Fennec. Spoofing as Chrome results in us rendering the video just fine,
      * and everything else works as well.
      */
-    {
+    id: "bug1509852",
+    platform: "android",
+    domain: "redbull.com",
+    bug: "1509852",
+    config: {
       matches: ["*://*.redbull.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1509873 - zmags.com - Add UA override for secure.viewer.zmags.com
      * WebCompat issue #21576 - https://webcompat.com/issues/21576
      *
      * The zmags viewer locks out Fennec with a "Browser unsupported" message,
      * but tests showed that it works just fine with a Chrome UA. Outreach
      * attempts were unsuccessful, and as the site has a relatively high rank,
      * we alter the UA.
      */
-    {
+    id: "bug1509873",
+    platform: "android",
+    domain: "zmags.com",
+    bug: "1509873",
+    config: {
       matches: ["*://*.viewer.zmags.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-  ],
-};
+  },
+]) {
+  UAOverrides.push(override);
+}
 
 /* globals browser */
 
 const UAHelpers = {
   getPrefix(originalUA) {
     return originalUA.substr(0, originalUA.indexOf(")") + 1);
   },
 };
 
-let activeListeners = [];
-function buildAndRegisterListener(matches, transformer) {
-  let listener = (details) => {
+const ActiveListeners = new Map();
+
+function enableOverride(override) {
+  if (override.active) {
+    return;
+  }
+
+  const {matches, uaTransformer} = override.config;
+  const listener = (details) => {
     for (var header of details.requestHeaders) {
       if (header.name.toLowerCase() === "user-agent") {
-        header.value = transformer(header.value);
+        header.value = uaTransformer(header.value);
       }
     }
     return {requestHeaders: details.requestHeaders};
   };
 
   browser.webRequest.onBeforeSendHeaders.addListener(
     listener,
     {urls: matches},
     ["blocking", "requestHeaders"]
   );
 
-  activeListeners.push(listener);
+  ActiveListeners.set(override, listener);
+  override.active = true;
 }
 
 async function registerUAOverrides() {
-  let platform = "desktop";
+  const platformMatches = ["all"];
   let platformInfo = await browser.runtime.getPlatformInfo();
-  if (platformInfo.os == "android") {
-    platform = "android";
+  platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
+
+  for (const override of UAOverrides) {
+    if (platformMatches.includes(override.platform)) {
+      override.availableOnPlatform = true;
+      enableOverride(override);
+    }
   }
-
-  let targetOverrides = UAOverrides.universal.concat(UAOverrides[platform]);
-  targetOverrides.forEach((override) => {
-    buildAndRegisterListener(override.matches, override.uaTransformer);
-  });
+  UAOverridesEnabled = true;
+  portsToAboutCompatTabs.broadcast({overridesChanged: filterOverrides(UAOverrides)});
 }
 
 function unregisterUAOverrides() {
-  activeListeners.forEach((listener) => {
-    browser.webRequest.onBeforeSendHeaders.removeListener(listener);
-  });
+  for (const override of UAOverrides) {
+    disableOverride(override);
+  }
+  UAOverridesEnabled = false;
+  portsToAboutCompatTabs.broadcast({overridesChanged: false});
+}
 
-  activeListeners = [];
+function disableOverride(override) {
+  if (!override.active) {
+    return;
+  }
+
+  browser.webRequest.onBeforeSendHeaders.removeListener(ActiveListeners.get(override));
+  override.active = false;
+  ActiveListeners.delete(override);
 }
 
 const OVERRIDE_PREF = "perform_ua_overrides";
 function checkOverridePref() {
   browser.aboutConfigPrefs.getPref(OVERRIDE_PREF).then(value => {
     if (value === undefined) {
       browser.aboutConfigPrefs.setPref(OVERRIDE_PREF, true);
     } else if (value === false) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/AboutCompat.jsm
@@ -0,0 +1,35 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["AboutCompat"];
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const addonID = "webcompat@mozilla.org";
+const addonPageRelativeURL = "/aboutCompat.html";
+
+function AboutCompat() {
+  this.chromeURL = WebExtensionPolicy.getByID(addonID).getURL(addonPageRelativeURL);
+}
+AboutCompat.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIAboutModule]),
+  getURIFlags() {
+    return Ci.nsIAboutModule.ALLOW_SCRIPT |
+           Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD;
+  },
+
+  newChannel(aURI, aLoadInfo) {
+    const uri = Services.io.newURI(this.chromeURL);
+    aLoadInfo.resultPrincipalURI = uri;
+    const channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+    channel.originalURI = aURI;
+
+    if (this.uriFlags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) {
+      channel.owner = null;
+    }
+    return channel;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/aboutCompat.css
@@ -0,0 +1,189 @@
+@media screen and (min-device-width:481px) {
+  :root {
+    font-family: sans-serif;
+    margin: 40px auto;
+    min-width: 30em;
+    max-width: 60em;
+  }
+
+  table {
+    width: 100%;
+    padding-bottom: 2em;
+  }
+
+  .float-right {
+    float: right;
+  }
+
+  .hidden {
+    display: none;
+  }
+
+  .table-title-container {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .wide-button {
+    display: block;
+    min-height: 32px;
+    padding-left: 30px;
+    padding-right: 30px;
+  }
+
+  .submitting {
+    background-image: url(chrome://global/skin/icons/loading.png);
+    background-position: center;
+    background-repeat: no-repeat;
+    background-size: 16px;
+  }
+
+  .submitting .submit-crash-button-label {
+    display: none;
+  }
+
+  .failed-to-submit {
+    color: #ca8695;
+  }
+
+  a.button-as-link {
+    -moz-appearance: none;
+    min-height: 30px;
+    color: var(--in-content-text-color) !important;
+    border: 1px solid var(--in-content-box-border-color) !important;
+    border-radius: 2px;
+    background-color: var(--in-content-page-background);
+    line-height: 30px;
+    margin: 4px 8px;
+    /* Ensure font-size isn't overridden by widget styling (e.g. in forms.css) */
+    font-size: 1em;
+  }
+
+  a.button-as-link:hover {
+    background-color: var(--in-content-box-background-hover) !important;
+    text-decoration: none;
+  }
+
+  h2.lighter-font-weight {
+    font-weight: lighter;
+  }
+
+  html[dir=ltr] th {
+    text-align: left;
+  }
+
+  html[dir=rtl] th {
+    text-align: right;
+  }
+}
+
+@media screen and (min-device-width:320px) and (max-device-width:480px) {
+  * {
+    margin: 0;
+    padding: 0;
+  }
+
+  html {
+    font-family: sans-serif;
+    font-size: 14px;
+    -moz-text-size-adjust: none;
+    background-color: #f5f5f5;
+  }
+
+  table, tr, p {
+    display: block;
+    background: #fff;
+  }
+
+  table {
+    border-top: 2px solid #0a84ff;
+    margin-top: -2px;
+    position: absolute;
+    width: 100%;
+    z-index: 1;
+    display: none;
+  }
+
+  tr {
+    position: relative;
+    border-bottom: 1px solid #d7d9db;
+    padding: 1em;
+  }
+
+  a {
+    color: #000;
+  }
+
+  .tab {
+    cursor: pointer;
+    position: relative;
+    z-index: 2;
+    display: inline-block;
+    text-align: left;
+    padding: 1em;
+    font-weight: bold;
+    border-top-left-radius: 3px;
+    border-top-right-radius: 3px;
+    border: 1px solid #d7d9db;
+    border-bottom: 0;
+    margin-bottom: 2px;
+    background: #f5f5f5;
+    color: #363b40;
+    font-size: 1em;
+    font-weight: bold;
+    padding: 1em;
+  }
+
+  .tab.active {
+    border-bottom-color: #fff;
+    background: #fff;
+    margin-bottom: 0;
+    padding-bottom: calc(1em + 2px);
+  }
+
+  .tab.active + table {
+    display: block;
+  }
+
+  td {
+    display: block;
+    position: relative;
+    padding-right: 6.5em;
+  }
+
+  td[colspan="4"] {
+    padding: 1em;
+    font-style: italic;
+    text-align: center;
+  }
+
+  td:not([colspan]):nth-child(1) {
+    font-weight: bold;
+  }
+
+  td:not([colspan]):nth-child(1) {
+    padding-bottom: 0.25em;
+  }
+
+  td:nth-child(3) {
+    display: contents;
+  }
+
+  button {
+    background: #e8e8e7;
+    position: absolute;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    width: 6em;
+    border: 0;
+    border-left: 1px solid #d7d9db;
+    -moz-appearance: none;
+    color: #000;
+  }
+
+  button::-moz-focus-inner {
+    border: 0;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/aboutCompat.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="stylesheet" href="aboutCompat.css" />
+  <link rel="stylesheet" media="screen and (min-device-width:481px), projection" type="text/css"
+          href="chrome://global/skin/in-content/common.css"/>
+  <link rel="localization" href="toolkit/about/aboutCompat.ftl"/>
+  <title data-l10n-id="text-title"></title>
+  <script src="aboutCompat.js"></script>
+  </head>
+<body>
+  <h2 class="tab active" data-l10n-id="label-overrides"></h2>
+  <table id="overrides">
+    <col/>
+    <col/>
+    <col/>
+  </table>
+  <h2 class="tab" data-l10n-id="label-interventions"></h2>
+  <table id="interventions">
+    <col/>
+    <col/>
+    <col/>
+  </table>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/aboutCompat.js
@@ -0,0 +1,146 @@
+/* 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";
+
+/* globals browser */
+
+const portToAddon = (function() {
+  let port;
+
+  function connect() {
+    port = browser.runtime.connect({name: "AboutCompatTab"});
+    port.onMessage.addListener(onMessageFromAddon);
+    port.onDisconnect.addListener(e => {
+      port = undefined;
+    });
+  }
+
+  connect();
+
+  async function send(message) {
+    if (port) {
+      return port.postMessage(message);
+    }
+    return Promise.reject("background script port disconnected");
+  }
+
+  return {send};
+}());
+
+const $ = function(sel) { return document.querySelector(sel); };
+
+const DOMContentLoadedPromise = new Promise(resolve => {
+  document.addEventListener("DOMContentLoaded", () => {
+    resolve();
+  }, {once: true});
+});
+
+Promise.all([
+  browser.runtime.sendMessage("getOverridesAndInterventions"),
+  DOMContentLoadedPromise,
+]).then(([info]) => {
+  document.body.addEventListener("click", async evt => {
+    const ele = evt.target;
+    if (ele.nodeName === "BUTTON") {
+      const row = ele.closest("[data-id]");
+      if (row) {
+        evt.preventDefault();
+        ele.disabled = true;
+        const id = row.getAttribute("data-id");
+        try {
+          await browser.runtime.sendMessage({command: "toggle", id});
+        } catch (_) {
+          ele.disabled = false;
+        }
+      }
+    } else if (ele.classList.contains("tab")) {
+      document.querySelectorAll(".tab").forEach(tab => {
+        tab.classList.remove("active");
+      });
+      ele.classList.add("active");
+    }
+  });
+
+  redraw(info);
+});
+
+function onMessageFromAddon(msg) {
+  if ("interventionsChanged" in msg) {
+    redrawTable($("#interventions"), msg.interventionsChanged);
+  }
+
+  if ("overridesChanged" in msg) {
+    redrawTable($("#overrides"), msg.overridesChanged);
+  }
+
+  const id = msg.toggling || msg.toggled;
+  const button = $(`[data-id="${id}"] button`);
+  if (!button) {
+    return;
+  }
+  const active = msg.active;
+  document.l10n.setAttributes(button,
+                      active ? "label-disable" : "label-enable");
+  button.disabled = !!msg.toggling;
+}
+
+function redraw(info) {
+  const {overrides, interventions} = info;
+  redrawTable($("#overrides"), overrides);
+  redrawTable($("#interventions"), interventions);
+}
+
+function redrawTable(table, data) {
+  const df = document.createDocumentFragment();
+  table.querySelectorAll("tr").forEach(tr => { tr.remove(); });
+
+  let noEntriesMessage;
+  if (data === false) {
+    noEntriesMessage = "text-disabled-in-about-config";
+  } else if (data.length === 0) {
+    noEntriesMessage = table.id === "overrides" ? "text-no-overrides"
+                                                : "text-no-interventions";
+  }
+
+  if (noEntriesMessage) {
+    const tr = document.createElement("tr");
+    df.appendChild(tr);
+
+    const td = document.createElement("td");
+    td.setAttribute("colspan", "3");
+    document.l10n.setAttributes(td, noEntriesMessage);
+    tr.appendChild(td);
+
+    table.appendChild(df);
+    return;
+  }
+
+  for (const row of data) {
+    const tr = document.createElement("tr");
+    tr.setAttribute("data-id", row.id);
+    df.appendChild(tr);
+
+    let td = document.createElement("td");
+    td.innerText = row.domain;
+    tr.appendChild(td);
+
+    td = document.createElement("td");
+    const a = document.createElement("a");
+    const bug = row.bug;
+    a.href = `https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`;
+    document.l10n.setAttributes(a, "label-more-information", {bug});
+    a.target = "aboutCompatBug";
+    td.appendChild(a);
+    tr.appendChild(td);
+
+    td = document.createElement("td");
+    tr.appendChild(td);
+    const button = document.createElement("button");
+    document.l10n.setAttributes(button,
+                        row.active ? "label-disable" : "label-enable");
+    td.appendChild(button);
+  }
+  table.appendChild(df);
+}
--- a/mobile/android/extensions/webcompat/aboutConfigPrefs.js
+++ b/mobile/android/extensions/webcompat/aboutConfigPrefs.js
@@ -1,17 +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/. */
 
 "use strict";
 
-/* global ExtensionAPI, ExtensionCommon */
+/* global ExtensionAPI, ExtensionCommon, Services, XPCOMUtils */
 
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
 
 this.aboutConfigPrefs = class extends ExtensionAPI {
   getAPI(context) {
     const EventManager = ExtensionCommon.EventManager;
     const extensionIDBase = context.extension.id.split("@")[0];
     const extensionPrefNameBase = `extensions.${extensionIDBase}.`;
 
     return {
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/aboutPage.js
@@ -0,0 +1,39 @@
+/* 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";
+
+/* global ExtensionAPI, Services, XPCOMUtils */
+
+ChromeUtils.defineModuleGetter(this, "AppConstants",
+                               "resource://gre/modules/AppConstants.jsm");
+
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "resProto",
+                                   "@mozilla.org/network/protocol;1?name=resource",
+                                   "nsISubstitutingProtocolHandler");
+
+const ResourceSubstitution = "webcompat";
+const ProcessScriptURL = "resource://webcompat/aboutPageProcessScript.js";
+
+const ShouldStart = ["default", "nightly", "nightly-try"].includes(AppConstants.MOZ_UPDATE_CHANNEL);
+
+this.aboutPage = class extends ExtensionAPI {
+  onStartup() {
+    const {rootURI} = this.extension;
+
+    resProto.setSubstitution(ResourceSubstitution,
+                             Services.io.newURI("chrome/res/", null, rootURI));
+
+    Services.ppmm.loadProcessScript(ProcessScriptURL, true);
+  }
+
+  onShutdown() {
+    resProto.setSubstitution(ResourceSubstitution, null);
+
+    Services.ppmm.removeDelayedProcessScript(ProcessScriptURL);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/aboutPage.json
@@ -0,0 +1,4 @@
+[{
+    "namespace": "aboutCompat",
+    "description": "Enables the about:compat page"
+}]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/aboutPageProcessScript.js
@@ -0,0 +1,22 @@
+/* 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";
+
+const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+const classID = Components.ID("{97bf9550-2a7b-11e9-b56e-0800200c9a66}");
+
+if (!Cm.isCIDRegistered(classID)) {
+  const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+  const factory = XPCOMUtils.generateSingletonFactory(function() {
+    const {AboutCompat} = ChromeUtils.import("resource://webcompat/AboutCompat.jsm");
+    return new AboutCompat();
+  });
+
+  Cm.registerFactory(classID, "about:compat",
+                     "@mozilla.org/network/protocol/about;1?what=compat",
+                     factory);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/background.js
@@ -0,0 +1,92 @@
+/* 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";
+
+/* global browser, disableInjection, disableOverride, enableInjection,
+          enableOverride, InjectionsEnabled, UAOverridesEnabled */
+
+const UAOverrides = [];
+const Injections = [];
+
+function getOverrideOrInterventionById(id) {
+  for (const [type, things] of Object.entries({
+    overrides: UAOverrides,
+    interventions: Injections,
+  })) {
+    for (const what of things) {
+      if (what.id === id) {
+        return {type, what};
+      }
+    }
+  }
+  return {};
+}
+
+const portsToAboutCompatTabs = (function() {
+  const ports = new Set();
+
+  browser.runtime.onConnect.addListener(port => {
+    ports.add(port);
+    port.onDisconnect.addListener(function() {
+      ports.delete(port);
+    });
+  });
+
+  async function broadcast(message) {
+    for (const port of ports) {
+      port.postMessage(message);
+    }
+  }
+
+  return {broadcast};
+}());
+
+function filterOverrides(overrides) {
+  return overrides.filter(override => override.availableOnPlatform).map(override => {
+    const {id, active, bug, domain} = override;
+    return {id, active, bug, domain};
+  });
+}
+
+browser.runtime.onMessage.addListener(msg => {
+  switch (msg.command || msg) {
+    case "toggle": {
+      const id = msg.id;
+      const {type, what} = getOverrideOrInterventionById(id);
+      if (!what) {
+        return Promise.reject(`No such override or intervention to toggle: ${id}`);
+      }
+      portsToAboutCompatTabs.broadcast({toggling: id, active: what.active}).then(async () => {
+        switch (type) {
+          case "interventions": {
+            if (what.active) {
+              await disableInjection(what);
+            } else {
+              await enableInjection(what);
+            }
+            break;
+          }
+          case "overrides": {
+            if (what.active) {
+              await disableOverride(what);
+            } else {
+              await enableOverride(what);
+            }
+            break;
+          }
+        }
+        portsToAboutCompatTabs.broadcast({toggled: id, active: what.active});
+      });
+      break;
+    }
+    case "getOverridesAndInterventions": {
+      return Promise.resolve({
+        overrides: UAOverridesEnabled && filterOverrides(UAOverrides) || false,
+        interventions: InjectionsEnabled && filterOverrides(Injections) || false,
+      });
+    }
+  }
+  return undefined;
+});
--- a/mobile/android/extensions/webcompat/injections.js
+++ b/mobile/android/extensions/webcompat/injections.js
@@ -1,95 +1,140 @@
 /**
  * For detailed information on our policies, and a documention on this format
  * and its possibilites, please check the Mozilla-Wiki at
  *
  * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
  */
-const contentScripts = {
-  universal: [
-    {
+
+"use strict";
+
+/* globals browser, filterOverrides, Injections, portsToAboutCompatTabs */
+
+let InjectionsEnabled = true;
+
+for (const injection of [
+  {
+    id: "testinjection",
+    platform: "all",
+    domain: "webcompat-addon-testcases.schub.io",
+    bug: "1287966",
+    contentScripts: {
       matches: ["*://webcompat-addon-testcases.schub.io/*"],
       css: [{file: "injections/css/bug0000000-dummy-css-injection.css"}],
       js: [{file: "injections/js/bug0000000-dummy-js-injection.js"}],
       runAt: "document_start",
     },
-  ],
-  desktop: [
-    {
+  }, {
+    id: "bug1452707",
+    platform: "desktop",
+    domain: "ib.absa.co.za",
+    bug: "1452707",
+    contentScripts: {
       matches: ["https://ib.absa.co.za/*"],
       js: [{file: "injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js"}],
       runAt: "document_start",
     },
-    {
+  }, {
+    id: "bug1457335",
+    platform: "desktop",
+    domain: "histography.io",
+    bug: "1457335",
+    contentScripts: {
       matches: ["*://histography.io/*"],
       js: [{file: "injections/js/bug1457335-histography.io-ua-change.js"}],
       runAt: "document_start",
     },
-    {
+  }, {
+    id: "bug1472075",
+    platform: "desktop",
+    domain: "bankofamerica.com",
+    bug: "1472075",
+    contentScripts: {
       matches: ["*://*.bankofamerica.com/*"],
       js: [{file: "injections/js/bug1472075-bankofamerica.com-ua-change.js"}],
       runAt: "document_start",
     },
-    {
+  }, {
+    id: "bug1472081",
+    platform: "desktop",
+    domain: "election.gov.np",
+    bug: "1472081",
+    contentScripts: {
       matches: ["http://202.166.205.141/bbvrs/*"],
       js: [{file: "injections/js/bug1472081-election.gov.np-window.sidebar-shim.js"}],
       runAt: "document_start",
       allFrames: true,
     },
-    {
+  }, {
+    id: "bug1482066",
+    platform: "desktop",
+    domain: "portalminasnet.com",
+    bug: "1482066",
+    contentScripts: {
       matches: ["*://portalminasnet.com/*"],
       js: [{file: "injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js"}],
       runAt: "document_start",
       allFrames: true,
     },
-  ],
-  android: [
-    {
-      matches: [
-        "*://*.ebay.at/*", "*://*.ebay.be/*", "*://*.ebay.ca/*", "*://*.ebay.ch/*",
-        "*://*.ebay.cn/*", "*://*.ebay.co.th/*", "*://*.ebay.co.uk/*", "*://*.ebay.com.au/*",
-        "*://*.ebay.com.hk/*", "*://*.ebay.com.my/*", "*://*.ebay.com.sg/*", "*://*.ebay.com.tw/*",
-        "*://*.ebay.com/*", "*://*.ebay.de/*", "*://*.ebay.es/*", "*://*.ebay.fr/*",
-        "*://*.ebay.ie/*", "*://*.ebay.in/*", "*://*.ebay.it/*", "*://*.ebay.nl/*",
-        "*://*.ebay.ph/*", "*://*.ebay.pl/*", "*://*.ebay.vn/*",
-      ],
-      js: [{file: "injections/js/bug1522755-ebay-scroll-anchoring.js"}],
-      runAt: "document_idle",
-    },
-  ],
-};
-
-/* globals browser */
+  },
+]) {
+  Injections.push(injection);
+}
 
 let port = browser.runtime.connect();
-let registeredContentScripts = [];
+const ActiveInjections = new Map();
 
 async function registerContentScripts() {
-  let platform = "desktop";
+  const platformMatches = ["all"];
   let platformInfo = await browser.runtime.getPlatformInfo();
-  if (platformInfo.os == "android") {
-    platform = "android";
+  platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
+
+  for (const injection of Injections) {
+    if (platformMatches.includes(injection.platform)) {
+      injection.availableOnPlatform = true;
+      await enableInjection(injection);
+    }
   }
 
-  let targetContentScripts = contentScripts.universal.concat(contentScripts[platform]);
-  targetContentScripts.forEach(async (contentScript) => {
-    try {
-      let handle = await browser.contentScripts.register(contentScript);
-      registeredContentScripts.push(handle);
-    } catch (ex) {
-      console.error("Registering WebCompat GoFaster content scripts failed: ", ex);
-    }
-  });
+  InjectionsEnabled = true;
+  portsToAboutCompatTabs.broadcast({interventionsChanged: filterOverrides(Injections)});
+}
+
+async function enableInjection(injection) {
+  if (injection.active) {
+    return;
+  }
+
+  try {
+    const handle = await browser.contentScripts.register(injection.contentScripts);
+    ActiveInjections.set(injection, handle);
+    injection.active = true;
+  } catch (ex) {
+    console.error("Registering WebCompat GoFaster content scripts failed: ", ex);
+  }
 }
 
 function unregisterContentScripts() {
-  registeredContentScripts.forEach((contentScript) => {
-    contentScript.unregister();
-  });
+  for (const injection of Injections) {
+    disableInjection(injection);
+  }
+  InjectionsEnabled = false;
+  portsToAboutCompatTabs.broadcast({interventionsChanged: false});
+}
+
+async function disableInjection(injection) {
+  if (!injection.active) {
+    return;
+  }
+
+  const contentScript = ActiveInjections.get(injection);
+  await contentScript.unregister();
+  ActiveInjections.delete(injection);
+  injection.active = false;
 }
 
 port.onMessage.addListener((message) => {
   switch (message.type) {
     case "injection-pref-changed":
       if (message.prefState) {
         registerContentScripts();
       } else {
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/webcompat/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+[features/webcompat@mozilla.org] chrome.jar:
+  res/AboutCompat.jsm (AboutCompat.jsm)
+  res/aboutPageProcessScript.js (aboutPageProcessScript.js)
--- a/mobile/android/extensions/webcompat/manifest.json
+++ b/mobile/android/extensions/webcompat/manifest.json
@@ -1,37 +1,46 @@
 {
   "manifest_version": 2,
   "name": "Web Compat",
   "description": "Urgent post-release fixes for web compatibility.",
-  "version": "3.0.1",
+  "version": "4.0.0",
 
   "applications": {
     "gecko": {
       "id": "webcompat@mozilla.org",
       "strict_min_version": "59.0b5"
     }
   },
 
   "experiment_apis": {
     "aboutConfigPrefs": {
       "schema": "aboutConfigPrefs.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "aboutConfigPrefs.js",
         "paths": [["aboutConfigPrefs"]]
       }
+    },
+    "aboutPage": {
+      "schema": "aboutPage.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "aboutPage.js",
+        "events": ["startup"]
+      }
     }
   },
 
   "permissions": [
     "webRequest",
     "webRequestBlocking",
     "<all_urls>"
   ],
 
   "background": {
     "scripts": [
+      "background.js",
       "injections.js",
       "ua_overrides.js"
     ]
   }
 }
--- a/mobile/android/extensions/webcompat/moz.build
+++ b/mobile/android/extensions/webcompat/moz.build
@@ -3,18 +3,24 @@
 # 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/.
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org'] += [
+  'aboutCompat.css',
+  'aboutCompat.html',
+  'aboutCompat.js',
   'aboutConfigPrefs.js',
   'aboutConfigPrefs.json',
+  'aboutPage.js',
+  'aboutPage.json',
+  'background.js',
   'injections.js',
   'manifest.json',
   'ua_overrides.js'
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['css'] += [
   'injections/css/bug0000000-dummy-css-injection.css'
 ]
@@ -24,10 +30,12 @@ FINAL_TARGET_FILES.features['webcompat@m
   'injections/js/bug1452707-window.controllers-shim-ib.absa.co.za.js',
   'injections/js/bug1457335-histography.io-ua-change.js',
   'injections/js/bug1472075-bankofamerica.com-ua-change.js',
   'injections/js/bug1472081-election.gov.np-window.sidebar-shim.js',
   'injections/js/bug1482066-portalminasnet.com-window.sidebar-shim.js',
   'injections/js/bug1522755-ebay-scroll-anchoring.js'
 ]
 
+JAR_MANIFESTS += ['jar.mn']
+
 with Files('**'):
   BUG_COMPONENT = ('Web Compatibility Tools', 'Go Faster')
--- a/mobile/android/extensions/webcompat/ua_overrides.js
+++ b/mobile/android/extensions/webcompat/ua_overrides.js
@@ -1,326 +1,417 @@
 /**
  * For detailed information on our policies, and a documention on this format
  * and its possibilites, please check the Mozilla-Wiki at
  *
  * https://wiki.mozilla.org/Compatibility/Go_Faster_Addon/Override_Policies_and_Workflows#User_Agent_overrides
  */
-const UAOverrides = {
-  universal: [
-    /*
-     * This is a dummy override that applies a Chrome UA to a dummy site that
-     * blocks all browsers but Chrome.
-     *
-     * This was only put in place to allow QA to test this system addon on an
-     * actual site, since we were not able to find a proper override in time.
-     */
-    {
+
+"use strict";
+
+/* globals filterOverrides, portsToAboutCompatTabs, UAOverrides */
+
+let UAOverridesEnabled = true;
+
+for (const override of [
+  {
+    id: "testoverride",
+    platform: "all",
+    domain: "webcompat-addon-testcases.schub.io",
+    bug: "1287966",
+    config: {
       matches: ["*://webcompat-addon-testcases.schub.io/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36";
       },
     },
-  ],
-  desktop: [
+  }, {
     /*
      * Bug 1464106 - directvnow.com - Create a UA override for Directvnow.com for playback on desktop
      * WebCompat issue #3846 - https://webcompat.com/issues/3846
      *
      * directvnow.com is blocking Firefox via UA sniffing. Outreach is still going
      * on, and playback works fine if we spoof as Chrome.
      */
-    {
+    id: "bug1464106",
+    platform: "desktop",
+    domain: "directvnow.com",
+    bug: "1464106",
+    config: {
       matches: ["*://*.directvnow.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36";
       },
     },
-  ],
-  android: [
+  }, {
     /*
      * Bug 1480710 - m.imgur.com - Build UA override
      * WebCompat issue #13154 - https://webcompat.com/issues/13154
      *
      * imgur returns a 404 for requests to CSS and JS file if requested with a Fennec
      * User Agent. By removing the Fennec identifies and adding Chrome Mobile's, we
      * receive the correct CSS and JS files.
      */
-    {
+    id: "bug1480710",
+    platform: "android",
+    domain: "m.imgur.com",
+    bug: "1480710",
+    config: {
       matches: ["*://m.imgur.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 755590 - sites.google.com - top bar doesn't show up in Firefox for Android
      *
      * Google Sites does show a different top bar template based on the User Agent.
      * For Fennec, this results in a broken top bar. Appending Chrome and Mobile Safari
      * identifiers to the UA results in a correct rendering.
      */
-    {
+    id: "bug755590",
+    platform: "android",
+    domain: "sites.google.com",
+    bug: "755590",
+    config: {
       matches: ["*://sites.google.com/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " Chrome/68.0.3440.85 Mobile Safari/537.366";
       },
     },
-
+  }, {
     /*
      * Bug 945963 - tieba.baidu.com serves simplified mobile content to Firefox Android
      * WebCompat issue #18455 - https://webcompat.com/issues/18455
      *
      * tieba.baidu.com and tiebac.baidu.com serve a heavily simplified and less functional
      * mobile experience to Firefox for Android users. Adding the AppleWebKit indicator
      * to the User Agent gets us the same experience.
      */
-    {
+    id: "bug945963",
+    platform: "android",
+    domain: "tieba.baidu.com",
+    bug: "945963",
+    config: {
       matches: ["*://tieba.baidu.com/*", "*://tiebac.baidu.com/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " AppleWebKit/537.36 (KHTML, like Gecko)";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "weather.yahoo.co.jp",
+    bug: "1177298",
+    config: {
       matches: ["*://weather.yahoo.co.jp/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "lohaco.jp",
+    bug: "1177298",
+    config: {
       matches: ["*://*.lohaco.jp/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "nhk.or.jp",
+    bug: "1177298",
+    config: {
       matches: ["*://*.nhk.or.jp/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " AppleWebKit";
       },
     },
-
+  }, {
     /*
      * Bug 1177298 - Write UA overrides for top Japanese Sites
      * (Imported from ua-update.json.in)
      *
      * To receive the proper mobile version instead of the desktop version or
      * a lower grade mobile experience, the UA is spoofed.
      */
-    {
+    id: "bug1177298",
+    platform: "android",
+    domain: "uniqlo.com",
+    bug: "1177298",
+    config: {
       matches: ["*://*.uniqlo.com/*"],
       uaTransformer: (originalUA) => {
         return originalUA + " Mobile Safari";
       },
     },
-
+  }, {
     /*
      * Bug 1338260 - Add UA override for directTV
      * (Imported from ua-update.json.in)
      *
      * DirectTV has issues with scrolling and cut-off images. Pretending to be
      * Chrome for Android fixes those issues.
      */
-    {
+    id: "bug1338260",
+    platform: "android",
+    domain: "directv.com",
+    bug: "1338260",
+    config: {
       matches: ["*://*.directv.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1385206 - Create UA override for rakuten.co.jp on Firefox Android
      * (Imported from ua-update.json.in)
      *
      * rakuten.co.jp serves a Desktop version if Firefox is included in the UA.
      */
-    {
+    id: "bug1385206",
+    platform: "android",
+    domain: "rakuten.co.jp",
+    bug: "1385206",
+    config: {
       matches: ["*://*.rakuten.co.jp/*"],
       uaTransformer: (originalUA) => {
         return originalUA.replace(/Firefox.+$/, "");
       },
     },
-
+  }, {
     /*
      * Bug 969844 - mobile.de sends desktop site to Firefox on Android
      *
      * mobile.de sends the desktop site to Fennec. Spooing as Chrome works fine.
      */
-    {
+    id: "bug969844",
+    platform: "android",
+    domain: "mobile.de",
+    bug: "969844",
+    config: {
       matches: ["*://*.mobile.de/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1476436 - mobile.bet365.com - add UA override for fennec
      * WebCompat issue #17010 - https://webcompat.com/issues/17010
      *
      * mobile.bet365.com serves fennec an alternative version with less interactive
      * elements, although they work just fine. Spoofing as Chrome makes the
      * interactive elements appear.
      */
-    {
+    id: "bug1476436",
+    platform: "android",
+    domain: "mobile.bet365.com",
+    bug: "1476436",
+    config: {
       matches: ["*://mobile.bet365.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1509831 - cc.com - Add UA override for CC.com
      * WebCompat issue #329 - https://webcompat.com/issues/329
      *
      * ComedyCentral blocks Firefox for not being able to play HLS, which was
      * true in previous versions, but no longer is. With a spoofed Chrome UA,
      * the site works just fine.
      */
-    {
+    id: "bug1509831",
+    platform: "android",
+    domain: "cc.com",
+    bug: "1509831",
+    config: {
       matches: ["*://*.cc.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1508564 - cnbc.com - Add UA override for videos on www.cnbc.com
      * WebCompat issue #8410 - https://webcompat.com/issues/8410
      *
      * The video framework loaded in via pdk.theplatform.com fails to
      * acknowledge that Firefox does support HLS, so it fails to find a
      * supported video format and shows the loading bar forever. Spoofing as
      * Chrome works.
      */
-    {
+    id: "bug1508564",
+    platform: "android",
+    domain: "cnbc.com",
+    bug: "1508564",
+    config: {
       matches: ["*://*.cnbc.com/*"],
       uaTransformer: (_) => {
         return "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1508516 - cineflix.com.br - Add UA override for cineflix.com.br/m/
      * WebCompat issue #21553 - https://webcompat.com/issues/21553
      *
      * The site renders a blank page with any Firefox snipped in the UA as it
      * is running into an exception. Spoofing as Chrome makes the site work
      * fine.
      */
-    {
+    id: "bug1508516",
+    platform: "android",
+    domain: "cineflix.com.br",
+    bug: "1508516",
+    config: {
       matches: ["*://*.cineflix.com.br/m/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1509852 - redbull.com - Add UA override for redbull.com
      * WebCompat issue #21439 - https://webcompat.com/issues/21439
      *
      * Redbull.com blocks some features, for example the live video player, for
      * Fennec. Spoofing as Chrome results in us rendering the video just fine,
      * and everything else works as well.
      */
-    {
+    id: "bug1509852",
+    platform: "android",
+    domain: "redbull.com",
+    bug: "1509852",
+    config: {
       matches: ["*://*.redbull.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-
+  }, {
     /*
      * Bug 1509873 - zmags.com - Add UA override for secure.viewer.zmags.com
      * WebCompat issue #21576 - https://webcompat.com/issues/21576
      *
      * The zmags viewer locks out Fennec with a "Browser unsupported" message,
      * but tests showed that it works just fine with a Chrome UA. Outreach
      * attempts were unsuccessful, and as the site has a relatively high rank,
      * we alter the UA.
      */
-    {
+    id: "bug1509873",
+    platform: "android",
+    domain: "zmags.com",
+    bug: "1509873",
+    config: {
       matches: ["*://*.viewer.zmags.com/*"],
       uaTransformer: (originalUA) => {
         return UAHelpers.getPrefix(originalUA) + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36";
       },
     },
-  ],
-};
+  },
+]) {
+  UAOverrides.push(override);
+}
 
 /* globals browser */
 
 const UAHelpers = {
   getPrefix(originalUA) {
     return originalUA.substr(0, originalUA.indexOf(")") + 1);
   },
 };
 
-let activeListeners = [];
-function buildAndRegisterListener(matches, transformer) {
-  let listener = (details) => {
+const ActiveListeners = new Map();
+
+function enableOverride(override) {
+  if (override.active) {
+    return;
+  }
+
+  const {matches, uaTransformer} = override.config;
+  const listener = (details) => {
     for (var header of details.requestHeaders) {
       if (header.name.toLowerCase() === "user-agent") {
-        header.value = transformer(header.value);
+        header.value = uaTransformer(header.value);
       }
     }
     return {requestHeaders: details.requestHeaders};
   };
 
   browser.webRequest.onBeforeSendHeaders.addListener(
     listener,
     {urls: matches},
     ["blocking", "requestHeaders"]
   );
 
-  activeListeners.push(listener);
+  ActiveListeners.set(override, listener);
+  override.active = true;
 }
 
 async function registerUAOverrides() {
-  let platform = "desktop";
+  const platformMatches = ["all"];
   let platformInfo = await browser.runtime.getPlatformInfo();
-  if (platformInfo.os == "android") {
-    platform = "android";
+  platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
+
+  for (const override of UAOverrides) {
+    if (platformMatches.includes(override.platform)) {
+      override.availableOnPlatform = true;
+      enableOverride(override);
+    }
   }
-
-  let targetOverrides = UAOverrides.universal.concat(UAOverrides[platform]);
-  targetOverrides.forEach((override) => {
-    buildAndRegisterListener(override.matches, override.uaTransformer);
-  });
+  UAOverridesEnabled = true;
+  portsToAboutCompatTabs.broadcast({overridesChanged: filterOverrides(UAOverrides)});
 }
 
 function unregisterUAOverrides() {
-  activeListeners.forEach((listener) => {
-    browser.webRequest.onBeforeSendHeaders.removeListener(listener);
-  });
+  for (const override of UAOverrides) {
+    disableOverride(override);
+  }
+  UAOverridesEnabled = false;
+  portsToAboutCompatTabs.broadcast({overridesChanged: false});
+}
 
-  activeListeners = [];
+function disableOverride(override) {
+  if (!override.active) {
+    return;
+  }
+
+  browser.webRequest.onBeforeSendHeaders.removeListener(ActiveListeners.get(override));
+  override.active = false;
+  ActiveListeners.delete(override);
 }
 
 const OVERRIDE_PREF = "perform_ua_overrides";
 function checkOverridePref() {
   browser.aboutConfigPrefs.getPref(OVERRIDE_PREF).then(value => {
     if (value === undefined) {
       browser.aboutConfigPrefs.setPref(OVERRIDE_PREF, true);
     } else if (value === false) {
--- a/mobile/android/locales/filter.py
+++ b/mobile/android/locales/filter.py
@@ -38,16 +38,19 @@ def test(mod, path, entity=None):
             # error on toolkit/about/*About.ftl
             return "error"
         if re.match(r"toolkit/about/[^/]*Plugins.ftl", path):
             # error on toolkit/about/*Plugins.ftl
             return "error"
         if re.match(r"toolkit/about/[^/]*Rights.ftl", path):
             # error on toolkit/about/*Rights.ftl
             return "error"
+        if re.match(r"toolkit/about/[^/]*Compat.ftl", path):
+            # error on toolkit/about/*Compat.ftl
+            return "error"
         if re.match(r"toolkit/about/[^/]*Support.ftl", path):
             # error on toolkit/about/*Support.ftl
             return "error"
         return "ignore"
 
     if mod == "dom":
         # keep this file list in sync with jar.mn
         if path in (
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -77,11 +77,13 @@ relativesrcdir toolkit/locales:
 #about:crashes
   crashreporter                                    (%crashreporter/**/*.ftl)
 #about:about
   toolkit/about                                    (%toolkit/about/*About.ftl)
 #about:support
   toolkit/about                                    (%toolkit/about/*Support.ftl)
 #about:rights
   toolkit/about                                    (%toolkit/about/*Rights.ftl)
+#about:compat
+  toolkit/about                                    (%toolkit/about/*Compat.ftl)
 #endif
 # Do not add files below the endif. Reviewers, expand more context above
 # for comments.
--- a/mobile/android/locales/l10n.toml
+++ b/mobile/android/locales/l10n.toml
@@ -177,16 +177,20 @@ exclude-multi-locale = [
     reference = "toolkit/locales/en-US/chrome/global/aboutRights.dtd"
     l10n = "{l}toolkit/chrome/global/aboutRights.dtd"
 
 [[paths]]
     reference = "toolkit/locales/en-US/toolkit/about/*Rights.ftl"
     l10n = "{l}toolkit/toolkit/about/*Rights.ftl"
 
 [[paths]]
+    reference = "toolkit/locales/en-US/toolkit/about/*Compat.ftl"
+    l10n = "{l}toolkit/toolkit/about/*Compat.ftl"
+
+[[paths]]
     reference = "toolkit/locales/en-US/chrome/global/charsetMenu.properties"
     l10n = "{l}toolkit/chrome/global/charsetMenu.properties"
 
 [[paths]]
     reference = "toolkit/locales/en-US/chrome/global/commonDialogs.properties"
     l10n = "{l}toolkit/chrome/global/commonDialogs.properties"
 
 [[paths]]
--- a/mobile/locales/filter.py
+++ b/mobile/locales/filter.py
@@ -39,16 +39,19 @@ def test(mod, path, entity=None):
             # error on toolkit/about/*About.ftl
             return "error"
         if re.match(r"toolkit/about/[^/]*Plugins.ftl", path):
             # error on toolkit/about/*Plugins.ftl
             return "error"
         if re.match(r"toolkit/about/[^/]*Rights.ftl", path):
             # error on toolkit/about/*Rights.ftl
             return "error"
+        if re.match(r"toolkit/about/[^/]*Compat.ftl", path):
+            # error on toolkit/about/*Compat.ftl
+            return "error"
         if re.match(r"toolkit/about/[^/]*Support.ftl", path):
             # error on toolkit/about/*Support.ftl
             return "error"
         return "ignore"
 
     if mod == "dom":
         # keep this file list in sync with jar.mn
         if path in (
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/toolkit/about/aboutCompat.ftl
@@ -0,0 +1,13 @@
+# 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/.
+
+label-disable = Disable
+label-enable = Enable
+label-interventions = Interventions
+label-more-information = More Information: Bug { $bug }
+label-overrides = User Agent Overrides
+text-disabled-in-about-config = This feature has been disabled in about:config
+text-no-interventions = No interventions are being used
+text-no-overrides = No UA overrides are being used
+text-title = about:compat