Bug 1451485 - convert webcompat-reporter to a webextension; r=aswan,Pike
authorThomas Wisniewski <wisniewskit@gmail.com>
Thu, 11 Oct 2018 15:53:23 +0000
changeset 496505 90b00e89e83e5eb07baf20dc55a8a141d8f242af
parent 496504 48b41b195cae79cae4aff526bd3c8b959f325185
child 496506 b57be7d7343c78e4484b03d98c4a1a21e03ea941
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan, Pike
bugs1451485
milestone64.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 1451485 - convert webcompat-reporter to a webextension; r=aswan,Pike convert webcompat-reporter to a webextension Differential Revision: https://phabricator.services.mozilla.com/D6575
browser/base/content/test/performance/browser_startup.js
browser/components/nsBrowserGlue.js
browser/extensions/webcompat-reporter/.eslintrc.js
browser/extensions/webcompat-reporter/background.js
browser/extensions/webcompat-reporter/bootstrap.js
browser/extensions/webcompat-reporter/content/WebCompatReporter.jsm
browser/extensions/webcompat-reporter/content/tab-frame.js
browser/extensions/webcompat-reporter/content/wc-frame.js
browser/extensions/webcompat-reporter/experimentalAPIs/aboutConfigPrefs.js
browser/extensions/webcompat-reporter/experimentalAPIs/aboutConfigPrefs.json
browser/extensions/webcompat-reporter/experimentalAPIs/browserInfo.js
browser/extensions/webcompat-reporter/experimentalAPIs/browserInfo.json
browser/extensions/webcompat-reporter/experimentalAPIs/l10n.js
browser/extensions/webcompat-reporter/experimentalAPIs/l10n.json
browser/extensions/webcompat-reporter/experimentalAPIs/pageActionExtras.js
browser/extensions/webcompat-reporter/experimentalAPIs/pageActionExtras.json
browser/extensions/webcompat-reporter/experimentalAPIs/tabExtras.js
browser/extensions/webcompat-reporter/experimentalAPIs/tabExtras.json
browser/extensions/webcompat-reporter/icons/lightbulb.svg
browser/extensions/webcompat-reporter/install.rdf.in
browser/extensions/webcompat-reporter/jar.mn
browser/extensions/webcompat-reporter/manifest.json
browser/extensions/webcompat-reporter/moz.build
browser/extensions/webcompat-reporter/skin/lightbulb.svg
browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
browser/extensions/webcompat-reporter/test/browser/browser_disabled_cleanup.js
browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
browser/extensions/webcompat-reporter/test/browser/head.js
browser/extensions/webcompat-reporter/test/browser/webcompat.html
browser/modules/PageActions.jsm
browser/modules/test/browser/head.js
--- a/browser/base/content/test/performance/browser_startup.js
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -54,17 +54,16 @@ const startupPhases = {
   // We reach this phase right after showing the first browser window.
   // This means that anything already loaded at this point has been loaded
   // before first paint and delayed it.
   "before first paint": {blacklist: {
     components: new Set([
       "nsSearchService.js",
     ]),
     modules: new Set([
-      "chrome://webcompat-reporter/content/WebCompatReporter.jsm",
       "chrome://webcompat/content/data/ua_overrides.jsm",
       "chrome://webcompat/content/lib/ua_overrider.jsm",
       "resource:///modules/AboutNewTab.jsm",
       "resource:///modules/BrowserUsageTelemetry.jsm",
       "resource:///modules/ContentCrashHandlers.jsm",
       "resource:///modules/ShellService.jsm",
       "resource://gre/modules/NewTabUtils.jsm",
       "resource://gre/modules/PageThumbs.jsm",
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1443,16 +1443,30 @@ BrowserGlue.prototype = {
       if (disabled) {
         await addon.disable({allowSystemAddons: true});
       } else {
         await addon.enable({allowSystemAddons: true});
       }
     });
   },
 
+  _monitorWebcompatReporterPref() {
+    const PREF = "extensions.webcompat-reporter.enabled";
+    const ID = "webcompat-reporter@mozilla.org";
+    Services.prefs.addObserver(PREF, async () => {
+      let addon = await AddonManager.getAddonByID(ID);
+      let enabled = Services.prefs.getBoolPref(PREF, false);
+      if (enabled && !addon.isActive) {
+        await addon.enable({allowSystemAddons: true});
+      } else if (!enabled && addon.isActive) {
+        await addon.disable({allowSystemAddons: true});
+      }
+    });
+  },
+
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     if (this._windowsWereRestored) {
       return;
     }
     this._windowsWereRestored = true;
 
     // Browser errors are only collected on Nightly, but telemetry for
@@ -1501,16 +1515,17 @@ BrowserGlue.prototype = {
         delete this._lateTasksIdleObserver;
         this._scheduleArbitrarilyLateIdleTasks();
       }
     };
     this._idleService.addIdleObserver(
       this._lateTasksIdleObserver, LATE_TASKS_IDLE_TIME_SEC);
 
     this._monitorScreenshotsPref();
+    this._monitorWebcompatReporterPref();
   },
 
   /**
    * Use this function as an entry point to schedule tasks that
    * need to run only once after startup, and can be scheduled
    * by using an idle callback.
    *
    * The functions scheduled here will fire from idle callbacks
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/.eslintrc.js
@@ -0,0 +1,108 @@
+"use strict";
+
+module.exports = {
+  "rules": {
+    // Rules from the mozilla plugin
+    "mozilla/balanced-listeners": "error",
+    "mozilla/no-aArgs": "error",
+    "mozilla/var-only-at-top-level": "error",
+
+    "valid-jsdoc": ["error", {
+      "prefer": {
+        "return": "returns",
+      },
+      "preferType": {
+        "Boolean": "boolean",
+        "Number": "number",
+        "String": "string",
+        "bool": "boolean",
+      },
+      "requireParamDescription": false,
+      "requireReturn": false,
+      "requireReturnDescription": false,
+    }],
+
+    // Forbid spaces inside the square brackets of array literals.
+    "array-bracket-spacing": ["error", "never"],
+
+    // Forbid spaces inside the curly brackets of object literals.
+    "object-curly-spacing": ["error", "never"],
+
+    // No space padding in parentheses
+    "space-in-parens": ["error", "never"],
+
+    // Require braces around blocks that start a new line
+    "curly": ["error", "all"],
+
+    // Two space indent
+    "indent-legacy": ["error", 2, {"SwitchCase": 1}],
+
+    // Always require parenthesis for new calls
+    "new-parens": "error",
+
+    // No expressions where a statement is expected
+    "no-unused-expressions": "error",
+
+    // No declaring variables that are never used
+    "no-unused-vars": "error",
+
+    // Disallow using variables outside the blocks they are defined (especially
+    // since only let and const are used, see "no-var").
+    "block-scoped-var": "error",
+
+    // Warn about cyclomatic complexity in functions.
+    "complexity": ["error", {"max": 26}],
+
+    // Enforce dots on the next line with property name.
+    "dot-location": ["error", "property"],
+
+    // Maximum length of a line.
+    // This should be 100 but too many lines were longer than that so set a
+    // conservative upper bound for now.
+    "max-len": ["error", 140],
+
+    // Maximum depth callbacks can be nested.
+    "max-nested-callbacks": ["error", 4],
+
+    // Allow the console API aside from console.log.
+    "no-console": ["error", {allow: ["error", "info", "trace", "warn"]}],
+
+    // Disallow fallthrough of case statements, except if there is a comment.
+    "no-fallthrough": "error",
+
+    // Disallow use of multiline strings (use template strings instead).
+    "no-multi-str": "error",
+
+    // Disallow multiple empty lines.
+    "no-multiple-empty-lines": ["error", {"max": 2}],
+
+    // Disallow usage of __proto__ property.
+    "no-proto": "error",
+
+    // Disallow use of assignment in return statement. It is preferable for a
+    // single line of code to have only one easily predictable effect.
+    "no-return-assign": "error",
+
+    // Disallow throwing literals (eg. throw "error" instead of
+    // throw new Error("error")).
+    "no-throw-literal": "error",
+
+    // Disallow padding within blocks.
+    "padded-blocks": ["error", "never"],
+
+    // Require use of the second argument for parseInt().
+    "radix": "error",
+
+    // Enforce spacing after semicolons.
+    "semi-spacing": ["error", {"before": false, "after": true}],
+
+    // Require "use strict" to be defined globally in the script.
+    "strict": ["error", "global"],
+
+    // Disallow Yoda conditions (where literal value comes first).
+    "yoda": "error",
+
+    // Disallow function or variable declarations in nested blocks
+    "no-inner-declarations": "error",
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/background.js
@@ -0,0 +1,127 @@
+/* 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 Config = {
+  newIssueEndpoint: "https://webcompat.com/issues/new",
+  newIssueEndpointPref: "newIssueEndpoint",
+  screenshotFormat: {
+    format: "jpeg",
+    quality: 75,
+  },
+};
+
+browser.pageActionExtras.setLabelForHistogram("webcompat");
+
+browser.pageAction.onClicked.addListener(tab => {
+  return getWebCompatInfoForTab(tab).then(
+    info => {
+      return openWebCompatTab(info);
+    },
+    err => {
+      console.error("WebCompat Reporter: unexpected error", err);
+    }
+  );
+});
+
+browser.aboutConfigPrefs.onEndpointPrefChange.addListener(checkEndpointPref);
+
+checkEndpointPref();
+
+async function checkEndpointPref() {
+  const value = await browser.aboutConfigPrefs.getEndpointPref();
+  if (value === undefined) {
+    browser.aboutConfigPrefs.setEndpointPref(Config.newIssueEndpoint);
+  } else {
+    Config.newIssueEndpoint = value;
+  }
+}
+
+function getWebCompatInfoForTab(tab) {
+  const {id, url} = tab;
+  return Promise.all([
+    browser.browserInfo.getBlockList(),
+    browser.browserInfo.getBuildID(),
+    browser.browserInfo.getGraphicsPrefs(),
+    browser.browserInfo.getUpdateChannel(),
+    browser.browserInfo.hasTouchScreen(),
+    browser.tabExtras.getWebcompatInfo(id),
+    browser.tabs.captureTab(id, Config.screenshotFormat).catch(e => {
+      console.error("WebCompat Reporter: getting a screenshot failed", e);
+      return Promise.resolve(undefined);
+    }),
+  ]).then(([blockList, buildID, graphicsPrefs, channel, hasTouchScreen,
+            frameInfo, screenshot]) => {
+    if (channel !== "linux") {
+      delete graphicsPrefs["layers.acceleration.force-enabled"];
+    }
+
+    const consoleLog = frameInfo.log;
+    delete frameInfo.log;
+
+    return Object.assign(frameInfo, {
+      tabId: id,
+      blockList,
+      details: Object.assign(graphicsPrefs, {
+        buildID,
+        channel,
+        consoleLog,
+        hasTouchScreen,
+        "mixed active content blocked": frameInfo.hasMixedActiveContentBlocked,
+        "mixed passive content blocked": frameInfo.hasMixedDisplayContentBlocked,
+        "tracking content blocked": frameInfo.hasTrackingContentBlocked ?
+                                    `true (${blockList})` : "false",
+      }),
+      screenshot,
+      url,
+    });
+  });
+}
+
+function stripNonASCIIChars(str) {
+  // eslint-disable-next-line no-control-regex
+  return str.replace(/[^\x00-\x7F]/g, "");
+}
+
+browser.l10n.getMessage("wc-reporter.label2").then(
+  browser.pageActionExtras.setDefaultTitle, () => {});
+
+browser.l10n.getMessage("wc-reporter.tooltip").then(
+  browser.pageActionExtras.setTooltipText, () => {});
+
+async function openWebCompatTab(compatInfo) {
+  const url = new URL(Config.newIssueEndpoint);
+  const {details} = compatInfo;
+  const params = {
+    url: `${compatInfo.url}`,
+    src: "desktop-reporter",
+    details,
+    label: [],
+  };
+  if (details["gfx.webrender.all"] || details["gfx.webrender.enabled"]) {
+    params.label.push("type-webrender-enabled");
+  }
+  if (compatInfo.hasTrackingContentBlocked) {
+    params.label.push(`type-tracking-protection-${compatInfo.blockList}`);
+  }
+
+  const tab = await browser.tabs.create({});
+  const json = stripNonASCIIChars(JSON.stringify(params));
+  await browser.tabExtras.loadURIWithPostData(tab.id, url.href, json,
+                                              "application/json");
+  await browser.tabs.executeScript(tab.id, {
+    runAt: "document_end",
+    code: `(function() {
+      async function sendScreenshot(dataURI) {
+        const res = await fetch(dataURI);
+        const blob = await res.blob();
+        postMessage(blob, "${url.origin}");
+      }
+      sendScreenshot("${compatInfo.screenshot}");
+    })()`,
+  });
+}
deleted file mode 100644
--- a/browser/extensions/webcompat-reporter/bootstrap.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* 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/. */
-
-/* global APP_SHUTDOWN:false */
-
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-const WEBCOMPATREPORTER_JSM = "chrome://webcompat-reporter/content/WebCompatReporter.jsm";
-const DELAYED_STARTUP_FINISHED = "browser-delayed-startup-finished";
-
-ChromeUtils.defineModuleGetter(this, "WebCompatReporter",
-  WEBCOMPATREPORTER_JSM);
-
-const PREF_WC_REPORTER_ENABLED = "extensions.webcompat-reporter.enabled";
-
-function requestReporterInit() {
-  Services.tm.idleDispatchToMainThread(function() {
-    WebCompatReporter.init();
-  });
-}
-
-function prefObserver(subject, topic, data) {
-  let enabled = Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED);
-  if (enabled) {
-    WebCompatReporter.init();
-  } else {
-    WebCompatReporter.uninit();
-  }
-}
-
-function onDelayedStartupFinished(subject, topic, data) {
-  requestReporterInit();
-  Services.obs.removeObserver(onDelayedStartupFinished,
-    DELAYED_STARTUP_FINISHED);
-}
-
-function startup(aData, aReason) {
-  // Observe pref changes and enable/disable as necessary.
-  Services.prefs.addObserver(PREF_WC_REPORTER_ENABLED, prefObserver);
-
-  // Only initialize if pref is enabled, after the delayed startup notification.
-  let enabled = Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED);
-  if (enabled) {
-    let win = Services.wm.getMostRecentWindow("navigator:browser");
-    if (win && win.gBrowserInit &&
-        win.gBrowserInit.delayedStartupFinished) {
-      requestReporterInit();
-    } else {
-      Services.obs.addObserver(onDelayedStartupFinished,
-        DELAYED_STARTUP_FINISHED);
-    }
-  }
-}
-
-function shutdown(aData, aReason) {
-  if (aReason === APP_SHUTDOWN) {
-    return;
-  }
-
-  Cu.unload(WEBCOMPATREPORTER_JSM);
-  Services.prefs.removeObserver(PREF_WC_REPORTER_ENABLED, prefObserver);
-}
-
-function install(aData, aReason) {}
-function uninstall(aData, aReason) {}
deleted file mode 100644
--- a/browser/extensions/webcompat-reporter/content/WebCompatReporter.jsm
+++ /dev/null
@@ -1,177 +0,0 @@
-/* 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 EXPORTED_SYMBOLS = ["WebCompatReporter"];
-
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-ChromeUtils.defineModuleGetter(this, "PageActions",
-  "resource:///modules/PageActions.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "wcStrings", function() {
-  return Services.strings.createBundle(
-    "chrome://webcompat-reporter/locale/webcompat.properties");
-});
-
-// Gather values for interesting details we want to appear in reports.
-let details = {};
-XPCOMUtils.defineLazyPreferenceGetter(details, "gfx.webrender.all", "gfx.webrender.all", false);
-XPCOMUtils.defineLazyPreferenceGetter(details, "gfx.webrender.blob-images", "gfx.webrender.blob-images", true);
-XPCOMUtils.defineLazyPreferenceGetter(details, "gfx.webrender.enabled", "gfx.webrender.enabled", false);
-XPCOMUtils.defineLazyPreferenceGetter(details, "image.mem.shared", "image.mem.shared", true);
-details.buildID = Services.appinfo.appBuildID;
-details.channel = AppConstants.MOZ_UPDATE_CHANNEL;
-
-Object.defineProperty(details, "blockList", {
-  // We don't want this property to end up in the stringified details
-  enumerable: false,
-  get() {
-    let trackingTable = Services.prefs.getCharPref("urlclassifier.trackingTable");
-    // If content-track-digest256 is in the tracking table,
-    // the user has enabled the strict list.
-    return trackingTable.includes("content") ? "strict" : "basic";
-  },
-});
-
-Object.defineProperty(details, "hasTouchScreen", {
-  enumerable: true,
-  get() {
-    // return a boolean to indicate there's a touch screen detected or not
-    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
-    return gfxInfo.getInfo().ApzTouchInput == 1;
-  },
-});
-
-if (AppConstants.platform == "linux") {
-  XPCOMUtils.defineLazyPreferenceGetter(details, "layers.acceleration.force-enabled", "layers.acceleration.force-enabled", false);
-}
-
-let WebCompatReporter = {
-  get endpoint() {
-    return Services.urlFormatter.formatURLPref(
-      "extensions.webcompat-reporter.newIssueEndpoint");
-  },
-
-  init() {
-    PageActions.addAction(new PageActions.Action({
-      id: "webcompat-reporter-button",
-      title: wcStrings.GetStringFromName("wc-reporter.label2"),
-      iconURL: "chrome://webcompat-reporter/skin/lightbulb.svg",
-      labelForHistogram: "webcompat",
-      onCommand: (e) => this.reportIssue(e.target.ownerGlobal),
-      onLocationChange: (window) => this.onLocationChange(window),
-    }));
-  },
-
-  uninit() {
-    let action = PageActions.actionForID("webcompat-reporter-button");
-    action.remove();
-  },
-
-  onLocationChange(window) {
-    let action = PageActions.actionForID("webcompat-reporter-button");
-    let scheme = window.gBrowser.currentURI.scheme;
-    let isReportable = ["http", "https"].includes(scheme);
-    action.setDisabled(!isReportable, window);
-  },
-
-  // This method injects a framescript that should send back a screenshot blob
-  // of the top-level window of the currently selected tab, and some other details
-  // about the tab (url, tracking protection + mixed content blocking status)
-  // resolved as a Promise.
-  getScreenshot(gBrowser) {
-    const FRAMESCRIPT = "chrome://webcompat-reporter/content/tab-frame.js";
-    const TABDATA_MESSAGE = "WebCompat:SendTabData";
-
-    return new Promise((resolve) => {
-      let mm = gBrowser.selectedBrowser.messageManager;
-      mm.loadFrameScript(FRAMESCRIPT, false);
-
-      mm.addMessageListener(TABDATA_MESSAGE, function receiveFn(message) {
-        mm.removeMessageListener(TABDATA_MESSAGE, receiveFn);
-        resolve([gBrowser, message.json]);
-      });
-    });
-  },
-
-  // This should work like so:
-  // 1) set up listeners for a new webcompat.com tab, and open it, passing
-  //    along the current URI
-  // 2) if we successfully got a screenshot from getScreenshot,
-  //    inject a frame script that will postMessage it to webcompat.com
-  //    so it can show a preview to the user and include it in FormData
-  // Note: openWebCompatTab arguments are passed in as an array because they
-  // are the result of a promise resolution.
-  openWebCompatTab([gBrowser, tabData]) {
-    const SCREENSHOT_MESSAGE = "WebCompat:SendScreenshot";
-    const FRAMESCRIPT = "chrome://webcompat-reporter/content/wc-frame.js";
-    let win = Services.wm.getMostRecentWindow("navigator:browser");
-    const WEBCOMPAT_ORIGIN = new win.URL(WebCompatReporter.endpoint).origin;
-
-    // Grab the relevant tab environment details that might change per site
-    details["mixed active content blocked"] = tabData.hasMixedActiveContentBlocked;
-    details["mixed passive content blocked"] = tabData.hasMixedDisplayContentBlocked;
-    details["tracking content blocked"] = tabData.hasTrackingContentBlocked ?
-      `true (${details.blockList})` : "false";
-
-      // question: do i add a label for basic vs strict?
-
-    let params = new URLSearchParams();
-    params.append("url", `${tabData.url}`);
-    params.append("src", "desktop-reporter");
-    params.append("details", JSON.stringify(details));
-
-    if (details["gfx.webrender.all"] || details["gfx.webrender.enabled"]) {
-      params.append("label", "type-webrender-enabled");
-    }
-
-    if (tabData.hasTrackingContentBlocked) {
-      params.append("label", `type-tracking-protection-${details.blockList}`);
-    }
-
-    let tab = gBrowser.loadOneTab(
-      `${WebCompatReporter.endpoint}?${params}`,
-      {inBackground: false, triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()});
-
-    // If we successfully got a screenshot blob, add a listener to know when
-    // the new tab is loaded before sending it over.
-    if (tabData && tabData.blob) {
-      let browser = gBrowser.getBrowserForTab(tab);
-      let loadedListener = {
-        QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
-          "nsISupportsWeakReference"]),
-        onStateChange(webProgress, request, flags, status) {
-          let isStopped = flags & Ci.nsIWebProgressListener.STATE_STOP;
-          let isNetwork = flags & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
-          if (isStopped && isNetwork && webProgress.isTopLevel) {
-            let location;
-            try {
-              location = request.QueryInterface(Ci.nsIChannel).URI;
-            } catch (ex) {}
-
-            if (location && location.prePath === WEBCOMPAT_ORIGIN) {
-              let mm = gBrowser.selectedBrowser.messageManager;
-              mm.loadFrameScript(FRAMESCRIPT, false);
-              mm.sendAsyncMessage(SCREENSHOT_MESSAGE, {
-                screenshot: tabData.blob,
-                origin: WEBCOMPAT_ORIGIN,
-              });
-
-              browser.removeProgressListener(this);
-            }
-          }
-        },
-      };
-
-      browser.addProgressListener(loadedListener);
-    }
-  },
-
-  reportIssue(global) {
-    this.getScreenshot(global.gBrowser).then(this.openWebCompatTab)
-                                       .catch(Cu.reportError);
-  },
-};
deleted file mode 100644
--- a/browser/extensions/webcompat-reporter/content/tab-frame.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* 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/. */
-
-/* eslint-env mozilla/frame-script */
-
-const TABDATA_MESSAGE = "WebCompat:SendTabData";
-
-let getScreenshot = function(win) {
-  return new Promise(resolve => {
-    let url = win.location.href;
-    try {
-      let dpr = win.devicePixelRatio;
-      let canvas = win.document.createElement("canvas");
-      let ctx = canvas.getContext("2d");
-      let x = win.document.documentElement.scrollLeft;
-      let y = win.document.documentElement.scrollTop;
-      let w = win.innerWidth;
-      let h = win.innerHeight;
-      canvas.width = dpr * w;
-      canvas.height = dpr * h;
-      ctx.scale(dpr, dpr);
-      ctx.drawWindow(win, x, y, w, h, "#fff");
-      canvas.toBlob(blob => {
-        resolve({
-          blob,
-          hasMixedActiveContentBlocked: docShell.hasMixedActiveContentBlocked,
-          hasMixedDisplayContentBlocked: docShell.hasMixedDisplayContentBlocked,
-          url,
-          hasTrackingContentBlocked: docShell.hasTrackingContentBlocked,
-        });
-      });
-    } catch (ex) {
-      // CanvasRenderingContext2D.drawWindow can fail depending on memory or
-      // surface size. Rather than reject, resolve the URL so the user can
-      // file an issue without a screenshot.
-      Cu.reportError(`WebCompatReporter: getting a screenshot failed: ${ex}`);
-      resolve({url});
-    }
-  });
-};
-
-getScreenshot(content).then(data => sendAsyncMessage(TABDATA_MESSAGE, data));
deleted file mode 100644
--- a/browser/extensions/webcompat-reporter/content/wc-frame.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* 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/. */
-
- /* eslint-env mozilla/frame-script */
-
-const SCREENSHOT_MESSAGE = "WebCompat:SendScreenshot";
-
-addMessageListener(SCREENSHOT_MESSAGE, function handleMessage(message) {
-  removeMessageListener(SCREENSHOT_MESSAGE, handleMessage);
-  // postMessage the screenshot blob from a content Sandbox so message event.origin
-  // is what we expect on the client-side (i.e., https://webcompat.com)
-  try {
-    let sb = new Cu.Sandbox(content.document.nodePrincipal);
-    sb.win = content;
-    sb.screenshotBlob = Cu.cloneInto(message.data.screenshot, content);
-    sb.wcOrigin = Cu.cloneInto(message.data.origin, content);
-    Cu.evalInSandbox("win.postMessage(screenshotBlob, wcOrigin);", sb);
-    Cu.nukeSandbox(sb);
-  } catch (ex) {
-    Cu.reportError(`WebCompatReporter: sending a screenshot failed: ${ex}`);
-  }
-});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/aboutConfigPrefs.js
@@ -0,0 +1,41 @@
+/* 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 */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+  getAPI(context) {
+    const EventManager = ExtensionCommon.EventManager;
+    const extensionIDBase = context.extension.id.split("@")[0];
+    const endpointPrefName = `extensions.${extensionIDBase}.newIssueEndpoint`;
+
+    return {
+      aboutConfigPrefs: {
+        onEndpointPrefChange: new EventManager({
+          context,
+          name: "aboutConfigPrefs.onEndpointPrefChange",
+          register: (fire) => {
+            const callback = () => {
+              fire.async().catch(() => {}); // ignore Message Manager disconnects
+            };
+            Services.prefs.addObserver(endpointPrefName, callback);
+            return () => {
+              Services.prefs.removeObserver(endpointPrefName, callback);
+            };
+          },
+        }).api(),
+        async getEndpointPref() {
+          return Services.prefs.getStringPref(endpointPrefName, undefined);
+        },
+        async setEndpointPref(value) {
+          Services.prefs.setStringPref(endpointPrefName, value);
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/aboutConfigPrefs.json
@@ -0,0 +1,35 @@
+[
+  {
+    "namespace": "aboutConfigPrefs",
+    "description": "experimental API extension to allow access to about:config preferences",
+    "events": [
+      {
+        "name": "onEndpointPrefChange",
+        "type": "function",
+        "parameters": []
+      }
+    ],
+    "functions": [
+      {
+        "name": "getEndpointPref",
+        "type": "function",
+        "description": "Get the endpoint preference's value",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "setEndpointPref",
+        "type": "function",
+        "description": "Set the endpoint preference's value",
+        "parameters": [
+          {
+            "name": "value",
+            "type": "string",
+            "description": "The new value"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/browserInfo.js
@@ -0,0 +1,54 @@
+/* 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 */
+
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+this.browserInfo = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      browserInfo: {
+        async getGraphicsPrefs() {
+          const prefs = {};
+          for (const [name, dflt] of Object.entries({
+            "layers.acceleration.force-enabled": false,
+            "gfx.webrender.all": false,
+            "gfx.webrender.blob-images": true,
+            "gfx.webrender.enabled": false,
+            "image.mem.shared": true,
+          })) {
+            prefs[name] = Services.prefs.getBoolPref(name, dflt);
+          }
+          return prefs;
+        },
+        async getAppVersion() {
+          return AppConstants.MOZ_APP_VERSION;
+        },
+        async getBlockList() {
+          const trackingTable = Services.prefs.getCharPref("urlclassifier.trackingTable");
+          // If content-track-digest256 is in the tracking table,
+          // the user has enabled the strict list.
+          return trackingTable.includes("content") ? "strict" : "basic";
+        },
+        async getBuildID() {
+          return Services.appinfo.appBuildID;
+        },
+        async getUpdateChannel() {
+          return AppConstants.MOZ_UPDATE_CHANNEL;
+        },
+        async getPlatform() {
+          return AppConstants.platform;
+        },
+        async hasTouchScreen() {
+          const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+          return gfxInfo.getInfo().ApzTouchInput == 1;
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/browserInfo.json
@@ -0,0 +1,57 @@
+[
+  {
+    "namespace": "browserInfo",
+    "description": "experimental API extensions to get browser info not exposed via web APIs",
+    "functions": [
+      {
+        "name": "getAppVersion",
+        "type": "function",
+        "description": "Gets the app version",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getBlockList",
+        "type": "function",
+        "description": "Gets the current blocklist",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getBuildID",
+        "type": "function",
+        "description": "Gets the build ID",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getGraphicsPrefs",
+        "type": "function",
+        "description": "Gets interesting about:config prefs for graphics",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getPlatform",
+        "type": "function",
+        "description": "Gets the platform",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getUpdateChannel",
+        "type": "function",
+        "description": "Gets the update channel",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "hasTouchScreen",
+        "type": "function",
+        "description": "Gets whether a touchscreen is present",
+        "parameters": [],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/l10n.js
@@ -0,0 +1,53 @@
+/* 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, XPCOMUtils */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "l10nStrings", function() {
+  return Services.strings.createBundle(
+    "chrome://webcompat-reporter/locale/webcompat.properties");
+});
+
+let l10nManifest;
+
+this.l10n = class extends ExtensionAPI {
+  onShutdown(reason) {
+    if (l10nManifest) {
+      Components.manager.removeBootstrappedManifestLocation(l10nManifest);
+    }
+  }
+  getAPI(context) {
+    // Until we move to Fluent (bug 1446164), we're stuck with
+    // chrome.manifest for handling localization since its what the
+    // build system can handle for localized repacks.
+    if (context.extension.rootURI instanceof Ci.nsIJARURI) {
+      l10nManifest = context.extension.rootURI.JARFile
+                            .QueryInterface(Ci.nsIFileURL).file;
+    } else if (context.extension.rootURI instanceof Ci.nsIFileURL) {
+      l10nManifest = context.extension.rootURI.file;
+    }
+
+    if (l10nManifest) {
+      Components.manager.addBootstrappedManifestLocation(l10nManifest);
+    } else {
+      Cu.reportError("Cannot find webcompat reporter chrome.manifest for registring translated strings");
+    }
+
+    return {
+      l10n: {
+        getMessage(name) {
+          try {
+            return Promise.resolve(l10nStrings.GetStringFromName(name));
+          } catch (e) {
+            return Promise.reject(e);
+          }
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/l10n.json
@@ -0,0 +1,19 @@
+[
+  {
+    "namespace": "l10n",
+    "description": "A stop-gap L10N API only meant to be used until a Fluent-based API is added in bug 1425104",
+    "functions": [
+      {
+        "name": "getMessage",
+        "type": "function",
+        "description": "Gets the message with the given name",
+        "parameters": [{
+          "name": "name",
+          "type": "string",
+          "description": "The name of the message"
+        }],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/pageActionExtras.js
@@ -0,0 +1,37 @@
+/* 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 */
+
+this.pageActionExtras = class extends ExtensionAPI {
+  getAPI(context) {
+    const extension = context.extension;
+    const pageActionAPI = extension.apiManager.getAPI("pageAction", extension,
+                                                      context.envType);
+    const {Management: {global: {windowTracker}}} =
+                ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
+    return {
+      pageActionExtras: {
+        async setDefaultTitle(title) {
+          pageActionAPI.defaults.title = title;
+          // Make sure the new default title is considered right away
+          for (const window of windowTracker.browserWindows()) {
+            const tab = window.gBrowser.selectedTab;
+            if (pageActionAPI.isShown(tab)) {
+              pageActionAPI.updateButton(window);
+            }
+          }
+        },
+        async setLabelForHistogram(label) {
+          pageActionAPI.browserPageAction._labelForHistogram = label;
+        },
+        async setTooltipText(text) {
+          pageActionAPI.browserPageAction.setTooltip(text);
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/pageActionExtras.json
@@ -0,0 +1,41 @@
+[
+  {
+    "namespace": "pageActionExtras",
+    "description": "experimental pageAction API extensions",
+    "functions": [
+      {
+        "name": "setDefaultTitle",
+        "type": "function",
+        "async": true,
+        "description": "Set the page action's title for all tabs",
+        "parameters": [{
+          "name": "title",
+          "type": "string",
+          "description": "title"
+        }]
+      },
+      {
+        "name": "setLabelForHistogram",
+        "type": "function",
+        "async": true,
+        "description": "Set the page action's label for telemetry histograms",
+        "parameters": [{
+          "name": "label",
+          "type": "string",
+          "description": "label for the histogram"
+        }]
+      },
+      {
+        "name": "setTooltipText",
+        "type": "function",
+        "async": true,
+        "description": "Set the page action's tooltip text",
+        "parameters": [{
+          "name": "text",
+          "type": "string",
+          "description": "text"
+        }]
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/tabExtras.js
@@ -0,0 +1,150 @@
+/* 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, XPCOMUtils */
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+function getInfoFrameScript(messageName) {
+  /* eslint-env mozilla/frame-script */
+
+  ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+  function getInnerWindowId(window) {
+    return window.windowUtils.currentInnerWindowID;
+  }
+
+  function getInnerWindowIDsForAllFrames(window) {
+    const innerWindowID = getInnerWindowId(window);
+    let ids = [innerWindowID];
+
+    if (window.frames) {
+      for (let i = 0; i < window.frames.length; i++) {
+        ids = ids.concat(getInnerWindowIDsForAllFrames(window.frames[i]));
+      }
+    }
+
+    return ids;
+  }
+
+  function getLoggedMessages(window, includePrivate = false) {
+    const ids = getInnerWindowIDsForAllFrames(window);
+    return getConsoleMessages(ids).concat(getScriptErrors(ids, includePrivate))
+                                  .sort((a, b) => a.timeStamp - b.timeStamp)
+                                  .map(m => m.message);
+  }
+
+  function getConsoleMessages(windowIds) {
+    const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
+                              .getService(Ci.nsIConsoleAPIStorage);
+    let messages = [];
+    for (const id of windowIds) {
+      messages = messages.concat(ConsoleAPIStorage.getEvents(id) || []);
+    }
+    return messages.map(evt => {
+      const {columnNumber, filename, level, lineNumber, timeStamp} = evt;
+      const args = evt.arguments.map(arg => {
+        return arg.toString();
+      });
+      const message = `[console.${level}(${args}) ${filename}:${lineNumber}:${columnNumber}]`;
+      return {timeStamp, message};
+    });
+  }
+
+  function getScriptErrors(windowIds, includePrivate = false) {
+    const messages = Services.console.getMessageArray() || [];
+    return messages.filter(message => {
+      if (message instanceof Ci.nsIScriptError) {
+        if (!includePrivate && message.isFromPrivateWindow) {
+          return false;
+        }
+
+        if (windowIds && !windowIds.includes(message.innerWindowID)) {
+          return false;
+        }
+
+        return true;
+      }
+
+      // If this is not an nsIScriptError and we need to do window-based
+      // filtering we skip this message.
+      return false;
+    }).map(error => {
+      const {timeStamp, message} = error;
+      return {timeStamp, message};
+    });
+  }
+
+  sendAsyncMessage(messageName, {
+    hasMixedActiveContentBlocked: docShell.hasMixedActiveContentBlocked,
+    hasMixedDisplayContentBlocked: docShell.hasMixedDisplayContentBlocked,
+    hasTrackingContentBlocked: docShell.hasTrackingContentBlocked,
+    log: getLoggedMessages(content),
+  });
+}
+
+this.tabExtras = class extends ExtensionAPI {
+  getAPI(context) {
+    const {tabManager} = context.extension;
+    const {Management: {global: {windowTracker}}} =
+                ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
+    return {
+      tabExtras: {
+        async loadURIWithPostData(tabId, url, postData, postDataContentType) {
+          const tab = tabManager.get(tabId);
+          if (!tab || !tab.browser) {
+            return Promise.reject("Invalid tab");
+          }
+
+          try {
+            new URL(url);
+          } catch (_) {
+            return Promise.reject("Invalid url");
+          }
+
+          if (typeof postData !== "string" && !(postData instanceof String)) {
+            return Promise.reject("postData must be a string");
+          }
+
+          const stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+                               .createInstance(Ci.nsIStringInputStream);
+          stringStream.data = postData;
+          const post = Cc["@mozilla.org/network/mime-input-stream;1"]
+                       .createInstance(Ci.nsIMIMEInputStream);
+          post.addHeader("Content-Type", postDataContentType ||
+                                         "application/x-www-form-urlencoded");
+          post.setData(stringStream);
+
+          return new Promise(resolve => {
+            const listener = {
+              onLocationChange(browser, webProgress, request, locationURI, flags) {
+                if (webProgress.isTopLevel && browser === tab.browser) {
+                  windowTracker.removeListener("progress", listener);
+                  resolve();
+                }
+              },
+            };
+            windowTracker.addListener("progress", listener);
+            tab.browser.webNavigation.loadURIWithOptions(url, null, null, null,
+                                                         post, null, null, null);
+          });
+        },
+        async getWebcompatInfo(tabId) {
+          return new Promise(resolve => {
+            const messageName = "WebExtension:GetWebcompatInfo";
+            const code = `${getInfoFrameScript.toString()};getInfoFrameScript("${messageName}")`;
+            const mm = tabManager.get(tabId).browser.messageManager;
+            mm.loadFrameScript(`data:,${encodeURI(code)}`, false);
+            mm.addMessageListener(messageName, function receiveFn(message) {
+              mm.removeMessageListener(messageName, receiveFn);
+              resolve(message.json);
+            });
+          });
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/experimentalAPIs/tabExtras.json
@@ -0,0 +1,39 @@
+[
+  {
+    "namespace": "tabExtras",
+    "description": "experimental tab API extensions",
+    "functions": [
+      {
+        "name": "getWebcompatInfo",
+        "type": "function",
+        "description": "Gets the content blocking status and script log for a given tab",
+        "parameters": [{
+          "type": "integer",
+          "name": "tabId",
+          "minimum": 0
+        }],
+        "async": true
+      },
+      {
+        "name": "loadURIWithPostData",
+        "type": "function",
+        "description": "Loads a URI on the given tab using a POST request",
+        "parameters": [{
+          "type": "integer",
+          "name": "tabId",
+          "minimum": 0
+        }, {
+          "type": "string",
+          "name": "url"
+        }, {
+          "type": "string",
+          "name": "postData"
+        }, {
+          "type": "string",
+          "name": "postDataContentType"
+        }],
+        "async": true
+      }
+    ]
+  }
+]
rename from browser/extensions/webcompat-reporter/skin/lightbulb.svg
rename to browser/extensions/webcompat-reporter/icons/lightbulb.svg
deleted file mode 100644
--- a/browser/extensions/webcompat-reporter/install.rdf.in
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0"?>
-<!-- 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/. -->
-
-#filter substitution
-
-<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-  <Description about="urn:mozilla:install-manifest">
-    <em:id>webcompat-reporter@mozilla.org</em:id>
-    <em:type>2</em:type>
-    <em:bootstrap>true</em:bootstrap>
-    <em:multiprocessCompatible>true</em:multiprocessCompatible>
-
-    <em:name>WebCompat Reporter</em:name>
-    <em:description>Report site compatibility issues on webcompat.com.</em:description>
-
-    <em:version>1.0.0</em:version>
-
-    <em:targetApplication>
-      <Description>
-        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
-        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-  </Description>
-</RDF>
deleted file mode 100644
--- a/browser/extensions/webcompat-reporter/jar.mn
+++ /dev/null
@@ -1,9 +0,0 @@
-# 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-reporter@mozilla.org] chrome.jar:
-% content webcompat-reporter %content/
-  content/ (content/*)
-% skin webcompat-reporter classic/1.0 %skin/
-  skin/  (skin/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/manifest.json
@@ -0,0 +1,77 @@
+{
+  "manifest_version": 2,
+  "name": "WebCompat Reporter",
+  "description": "Report site compatibility issues on webcompat.com",
+  "author": "Thomas Wisniewski <twisniewski@mozilla.com>",
+  "version": "1.1.0",
+  "homepage_url": "https://github.com/mozilla/webcompat-reporter",
+  "applications": {
+    "gecko": {
+      "id": "webcompat-reporter@mozilla.org"
+    }
+  },
+  "experiment_apis": {
+    "aboutConfigPrefs": {
+      "schema": "experimentalAPIs/aboutConfigPrefs.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/aboutConfigPrefs.js",
+        "paths": [["aboutConfigPrefs"]]
+      }
+    },
+    "browserInfo": {
+      "schema": "experimentalAPIs/browserInfo.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/browserInfo.js",
+        "paths": [["browserInfo"]]
+      }
+    },
+    "l10n": {
+      "schema": "experimentalAPIs/l10n.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/l10n.js",
+        "paths": [["l10n"]]
+      }
+    },
+    "pageActionExtras": {
+      "schema": "experimentalAPIs/pageActionExtras.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/pageActionExtras.js",
+        "paths": [["pageActionExtras"]]
+      }
+    },
+    "tabExtras": {
+      "schema": "experimentalAPIs/tabExtras.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/tabExtras.js",
+        "paths": [["tabExtras"]]
+      }
+    }
+  },
+  "icons": {
+    "16": "icons/lightbulb.svg",
+    "32": "icons/lightbulb.svg",
+    "48": "icons/lightbulb.svg",
+    "96": "icons/lightbulb.svg",
+    "128": "icons/lightbulb.svg"
+  },
+  "permissions": [
+    "tabs",
+    "<all_urls>"
+  ],
+  "background": {
+    "scripts": [
+      "background.js"
+    ]
+  },
+  "page_action": {
+    "browser_style": true,
+    "default_icon": "icons/lightbulb.svg",
+    "default_title": "Report Site Issue…",
+    "show_matches": ["http://*/*", "https://*/*"]
+  }
+}
--- a/browser/extensions/webcompat-reporter/moz.build
+++ b/browser/extensions/webcompat-reporter/moz.build
@@ -5,21 +5,33 @@
 # 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']
 
 DIRS += ['locales']
 
 FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'] += [
-  'bootstrap.js'
+  'background.js',
+  'manifest.json'
 ]
 
-FINAL_TARGET_PP_FILES.features['webcompat-reporter@mozilla.org'] += [
-  'install.rdf.in'
+FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'].experimentalAPIs += [
+  'experimentalAPIs/aboutConfigPrefs.js',
+  'experimentalAPIs/aboutConfigPrefs.json',
+  'experimentalAPIs/browserInfo.js',
+  'experimentalAPIs/browserInfo.json',
+  'experimentalAPIs/l10n.js',
+  'experimentalAPIs/l10n.json',
+  'experimentalAPIs/pageActionExtras.js',
+  'experimentalAPIs/pageActionExtras.json',
+  'experimentalAPIs/tabExtras.js',
+  'experimentalAPIs/tabExtras.json'
 ]
 
-JAR_MANIFESTS += ['jar.mn']
+FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'].icons += [
+  'icons/lightbulb.svg'
+]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('Web Compatibility Tools', 'General')
--- a/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
@@ -1,46 +1,51 @@
+"use strict";
+
 const REPORTABLE_PAGE = "http://example.com/";
 const REPORTABLE_PAGE2 = "https://example.com/";
 const NONREPORTABLE_PAGE = "about:mozilla";
 
-/* Test that the Report Site Issue button is enabled for http and https tabs,
+/* Test that the Report Site Issue panel item is enabled for http and https tabs,
    on page load, or TabSelect, and disabled for everything else. */
 add_task(async function test_button_state_disabled() {
   await SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENABLED, true]]});
 
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE);
   await openPageActions();
-  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
+  is(await isPanelItemEnabled(), true, "Check that panel item is enabled for reportable schemes on tab load");
 
   let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, NONREPORTABLE_PAGE);
   await openPageActions();
-  is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on tab load");
+  is(await isPanelItemDisabled(), true, "Check that panel item is disabled for non-reportable schemes on tab load");
 
   let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE2);
   await openPageActions();
-  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
+  is(await isPanelItemEnabled(), true, "Check that panel item is enabled for reportable schemes on tab load");
 
-  BrowserTestUtils.removeTab(tab1);
-  BrowserTestUtils.removeTab(tab2);
-  BrowserTestUtils.removeTab(tab3);
+  await BrowserTestUtils.removeTab(tab1);
+  await BrowserTestUtils.removeTab(tab2);
+  await BrowserTestUtils.removeTab(tab3);
 });
 
 /* Test that the button is enabled or disabled when we expected it to be, when
    pinned to the URL bar. */
 add_task(async function test_button_state_in_urlbar() {
   await SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENABLED, true]]});
 
   pinToURLBar();
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE);
-  is(isURLButtonEnabled(), true, "Check that button (in urlbar) is enabled for reportable schemes on tab load");
+  await openPageActions();
+  is(await isURLButtonPresent(), true, "Check that urlbar icon is enabled for reportable schemes on tab load");
 
   let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, NONREPORTABLE_PAGE);
-  is(isURLButtonEnabled(), false, "Check that button (in urlbar) is hidden for non-reportable schemes on tab load");
+  await openPageActions();
+  is(await isURLButtonPresent(), false, "Check that urlbar icon is hidden for non-reportable schemes on tab load");
 
   let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE2);
-  is(isURLButtonEnabled(), true, "Check that button (in urlbar) is enabled for reportable schemes on tab load");
+  await openPageActions();
+  is(await isURLButtonPresent(), true, "Check that urlbar icon is enabled for reportable schemes on tab load");
 
   unpinFromURLBar();
-  BrowserTestUtils.removeTab(tab1);
-  BrowserTestUtils.removeTab(tab2);
-  BrowserTestUtils.removeTab(tab3);
+  await BrowserTestUtils.removeTab(tab1);
+  await BrowserTestUtils.removeTab(tab2);
+  await BrowserTestUtils.removeTab(tab3);
 });
--- a/browser/extensions/webcompat-reporter/test/browser/browser_disabled_cleanup.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_disabled_cleanup.js
@@ -1,8 +1,31 @@
+"use strict";
+
 // Test the addon is cleaning up after itself when disabled.
 add_task(async function test_disabled() {
-  await SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENABLED, false]]});
+  await promiseAddonEnabled();
+
+  pinToURLBar();
+
+  SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false);
+  await promisePageActionRemoved();
+
+  await BrowserTestUtils.withNewTab({gBrowser, url: "http://example.com"}, async function() {
+    await openPageActions();
+    is(await isPanelItemPresent(), false, "Report Site Issue button is not shown on the popup panel.");
+    is(await isURLButtonPresent(), false, "Report Site Issue is not shown on the url bar.");
+  });
 
-  await BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function() {
-    is(document.getElementById("webcompat-reporter-button"), null, "Report Site Issue button does not exist.");
+  await promiseAddonEnabled();
+
+  pinToURLBar();
+
+  await BrowserTestUtils.withNewTab({gBrowser, url: "http://example.com"}, async function() {
+    await openPageActions();
+    is(await isPanelItemEnabled(), true, "Report Site Issue button is shown on the popup panel.");
+    is(await isURLButtonPresent(), true, "Report Site Issue is shown on the url bar.");
   });
+
+  // Shut down the addon at the end,or the new instance started when we re-enabled it will "leak".
+  SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false);
+  await promisePageActionRemoved();
 });
--- a/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
@@ -1,46 +1,84 @@
+"use strict";
+
 /* Test that clicking on the Report Site Issue button opens a new tab
    and sends a postMessaged blob to it. */
 add_task(async function test_opened_page() {
   requestLongerTimeout(2);
 
+  const serverLanding = await startIssueServer();
+
   // ./head.js sets the value for PREF_WC_REPORTER_ENDPOINT
   await SpecialPowers.pushPrefEnv({set: [
     [PREF_WC_REPORTER_ENABLED, true],
-    [PREF_WC_REPORTER_ENDPOINT, NEW_ISSUE_PAGE],
+    [PREF_WC_REPORTER_ENDPOINT, serverLanding],
   ]});
 
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
 
   await openPageActions();
-  let webcompatButton = document.getElementById(WC_PAGE_ACTION_ID);
-  ok(webcompatButton, "Report Site Issue button exists.");
+  await isPanelItemEnabled();
 
   let screenshotPromise;
   let newTabPromise = new Promise(resolve => {
     gBrowser.tabContainer.addEventListener("TabOpen", event => {
       let tab = event.target;
       screenshotPromise = BrowserTestUtils.waitForContentEvent(
         tab.linkedBrowser, "ScreenshotReceived", false, null, true);
       resolve(tab);
-    }, { once: true });
+    }, {once: true});
   });
-  webcompatButton.click();
+  document.getElementById(WC_PAGE_ACTION_PANEL_ID).click();
   let tab2 = await newTabPromise;
   await screenshotPromise;
 
-  await ContentTask.spawn(tab2.linkedBrowser, {TEST_PAGE}, function(args) {
+  await ContentTask.spawn(tab2.linkedBrowser, {TEST_PAGE}, async function(args) {
+    async function isGreen(dataUrl) {
+      const getPixel = await new Promise(resolve => {
+        const myCanvas = content.document.createElement("canvas");
+        const ctx = myCanvas.getContext("2d");
+        const img = new content.Image();
+        img.onload = () => {
+          ctx.drawImage(img, 0, 0);
+          resolve((x, y) => {
+            return ctx.getImageData(x, y, 1, 1).data;
+          });
+        };
+        img.src = dataUrl;
+      });
+      function isPixelGreenFuzzy(p) { // jpeg, so it will be off slightly
+        const fuzz = 4;
+        return p[0] < fuzz && Math.abs(p[1] - 128) < fuzz && p[2] < fuzz;
+      }
+      ok(isPixelGreenFuzzy(getPixel(0, 0)), "The pixels were green");
+    }
+
     let doc = content.document;
     let urlParam = doc.getElementById("url").innerText;
     let preview = doc.getElementById("screenshot-preview");
     is(urlParam, args.TEST_PAGE, "Reported page is correctly added to the url param");
 
     let detailsParam = doc.getElementById("details").innerText;
-    ok(typeof JSON.parse(detailsParam) == "object", "Details param is a stringified JSON object.");
+    const details = JSON.parse(detailsParam);
+    ok(typeof details == "object", "Details param is a stringified JSON object.");
+    ok(Array.isArray(details.consoleLog), "Details has a consoleLog array.");
+    ok(typeof details.buildID == "string", "Details has a buildID string.");
+    ok(typeof details.channel == "string", "Details has a channel string.");
+    ok(typeof details.hasTouchScreen == "boolean", "Details has a hasTouchScreen flag.");
+    ok(typeof details["mixed active content blocked"] == "boolean", "Details has a mixed active content blocked flag.");
+    ok(typeof details["mixed passive content blocked"] == "boolean", "Details has a mixed passive content blocked flag.");
+    ok(typeof details["tracking content blocked"] == "string", "Details has a tracking content blocked string.");
+    ok(typeof details["gfx.webrender.all"] == "boolean", "Details has gfx.webrender.all.");
+    ok(typeof details["gfx.webrender.blob-images"] == "boolean", "Details has gfx.webrender.blob-images.");
+    ok(typeof details["gfx.webrender.enabled"] == "boolean", "Details has gfx.webrender.enabled.");
+    ok(typeof details["image.mem.shared"] == "boolean", "Details has image.mem.shared.");
 
     is(preview.innerText, "Pass", "A Blob object was successfully transferred to the test page.");
-    ok(preview.style.backgroundImage.startsWith("url(\"data:image/png;base64,iVBOR"), "A green screenshot was successfully postMessaged");
+
+    const bgUrl = preview.style.backgroundImage.match(/url\(\"(.*)\"\)/)[1];
+    ok(bgUrl.startsWith("data:image/jpeg;base64,"), "A jpeg screenshot was successfully postMessaged");
+    await isGreen(bgUrl);
   });
 
   BrowserTestUtils.removeTab(tab2);
   BrowserTestUtils.removeTab(tab1);
 });
--- a/browser/extensions/webcompat-reporter/test/browser/head.js
+++ b/browser/extensions/webcompat-reporter/test/browser/head.js
@@ -1,23 +1,95 @@
+"use strict";
+
+ChromeUtils.defineModuleGetter(this, "AddonManager",
+                               "resource://gre/modules/AddonManager.jsm");
+
+const {Management} = ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
+
 const PREF_WC_REPORTER_ENABLED = "extensions.webcompat-reporter.enabled";
 const PREF_WC_REPORTER_ENDPOINT = "extensions.webcompat-reporter.newIssueEndpoint";
 
 const TEST_ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
 const TEST_PAGE = TEST_ROOT + "test.html";
 const NEW_ISSUE_PAGE = TEST_ROOT + "webcompat.html";
 
-const WC_PAGE_ACTION_ID = "pageAction-panel-webcompat-reporter-button";
-const WC_PAGE_ACTION_URLBAR_ID = "pageAction-urlbar-webcompat-reporter-button";
+const WC_ADDON_ID = "webcompat-reporter@mozilla.org";
+
+const WC_PAGE_ACTION_ID = "webcompat-reporter_mozilla_org";
+const WC_PAGE_ACTION_PANEL_ID = "pageAction-panel-webcompat-reporter_mozilla_org";
+const WC_PAGE_ACTION_URLBAR_ID = "pageAction-urlbar-webcompat-reporter_mozilla_org";
+
 
-function isButtonDisabled() {
-  return document.getElementById(WC_PAGE_ACTION_ID).disabled;
+const oldPAadd = PageActions.addAction;
+const placedSignal = "webext-page-action-placed";
+PageActions.addAction = function(action) {
+  oldPAadd.call(this, action);
+  if (action.id === WC_PAGE_ACTION_ID) {
+    Services.obs.notifyObservers(null, placedSignal);
+  }
+  return action;
+};
+const oldPAremoved = PageActions.onActionRemoved;
+const removedSignal = "webext-page-action-removed";
+PageActions.onActionRemoved = function(action) {
+  oldPAremoved.call(this, action);
+  if (action.id === WC_PAGE_ACTION_ID) {
+    Services.obs.notifyObservers(null, removedSignal);
+  }
+  return action;
+};
+
+function promisePageActionSignal(signal) {
+  return new Promise(done => {
+    const obs = function() {
+      Services.obs.removeObserver(obs, signal);
+      done();
+    };
+    Services.obs.addObserver(obs, signal);
+  });
 }
 
-function isURLButtonEnabled() {
+function promisePageActionPlaced() {
+  return promisePageActionSignal(placedSignal);
+}
+
+function promisePageActionRemoved() {
+  return promisePageActionSignal(removedSignal);
+}
+
+
+async function promiseAddonEnabled() {
+  const addon = await AddonManager.getAddonByID(WC_ADDON_ID);
+  if (addon.isActive) {
+    return;
+  }
+  const pref = SpecialPowers.Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED, false);
+  if (!pref) {
+    SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, true);
+  }
+  await promisePageActionPlaced();
+}
+
+
+async function isPanelItemEnabled() {
+  const icon = document.getElementById(WC_PAGE_ACTION_PANEL_ID);
+  return icon && !icon.disabled;
+}
+
+async function isPanelItemDisabled() {
+  const icon = document.getElementById(WC_PAGE_ACTION_PANEL_ID);
+  return icon && icon.disabled;
+}
+
+async function isPanelItemPresent() {
+  return document.getElementById(WC_PAGE_ACTION_PANEL_ID) !== null;
+}
+
+async function isURLButtonPresent() {
   return document.getElementById(WC_PAGE_ACTION_URLBAR_ID) !== null;
 }
 
 function openPageActions() {
   let dwu = window.windowUtils;
   return BrowserTestUtils.waitForCondition(() => {
     // Wait for the main page action button to become visible.  It's hidden for
     // some URIs, so depending on when this is called, it may not yet be quite
@@ -43,17 +115,17 @@ function openPageActions() {
 function promisePageActionPanelShown() {
   return new Promise(resolve => {
     if (BrowserPageActions.panelNode.state == "open") {
       executeSoon(resolve);
       return;
     }
     BrowserPageActions.panelNode.addEventListener("popupshown", () => {
       executeSoon(resolve);
-    }, { once: true });
+    }, {once: true});
   });
 }
 
 function promisePageActionViewChildrenVisible(panelViewNode) {
   info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
   let dwu = window.windowUtils;
   return BrowserTestUtils.waitForCondition(() => {
     let bodyNode = panelViewNode.firstElementChild;
@@ -63,14 +135,60 @@ function promisePageActionViewChildrenVi
         return true;
       }
     }
     return false;
   });
 }
 
 function pinToURLBar() {
-  PageActions.actionForID("webcompat-reporter-button").pinnedToUrlbar = true;
+  PageActions.actionForID(WC_PAGE_ACTION_ID).pinnedToUrlbar = true;
 }
 
 function unpinFromURLBar() {
-  PageActions.actionForID("webcompat-reporter-button").pinnedToUrlbar = false;
+  PageActions.actionForID(WC_PAGE_ACTION_ID).pinnedToUrlbar = false;
 }
+
+async function startIssueServer() {
+  const BinaryInputStream =
+    Components.Constructor("@mozilla.org/binaryinputstream;1",
+                           "nsIBinaryInputStream", "setInputStream");
+  function getRequestData(request) {
+    const body = new BinaryInputStream(request.bodyInputStream);
+    const bytes = [];
+    let avail;
+    while ((avail = body.available()) > 0) {
+      Array.prototype.push.apply(bytes, body.readByteArray(avail));
+    }
+    return String.fromCharCode.apply(null, bytes);
+  }
+
+  const landingTemplate = await new Promise((resolve, reject) => {
+    const xhr = new XMLHttpRequest();
+    xhr.open("GET", NEW_ISSUE_PAGE);
+    xhr.onload = () => {
+      resolve(xhr.responseText);
+    };
+    xhr.onerror = reject;
+    xhr.send();
+  });
+
+  const {HttpServer} = ChromeUtils.import("resource://testing-common/httpd.js", {});
+  const server = new HttpServer();
+
+  registerCleanupFunction(async function cleanup() {
+    await new Promise(resolve => server.stop(resolve));
+  });
+
+  server.registerPathHandler("/new", function(request, response) {
+    response.setHeader("Content-Type", "text/html", false);
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    const postData = JSON.parse(getRequestData(request));
+    const url = postData.url;
+    const details = JSON.stringify(postData.details);
+    const output = landingTemplate.replace("$$URL$$", url)
+                                  .replace("$$DETAILS$$", details);
+    response.write(output);
+  });
+
+  server.start(-1);
+  return `http://localhost:${server.identity.primaryPort}/new`;
+}
--- a/browser/extensions/webcompat-reporter/test/browser/webcompat.html
+++ b/browser/extensions/webcompat-reporter/test/browser/webcompat.html
@@ -1,32 +1,30 @@
 <!DOCTYPE html>
 <meta charset="utf-8">
 <style>
  #screenshot-preview {width: 200px; height: 200px;}
 </style>
-<div id="url"></div>
-<div id="details"></div>
+<div id="url">$$URL$$</div>
+<div id="details">$$DETAILS$$</div>
 <div id="screenshot-preview">Fail</div>
 <script>
-let params = new URL(location.href).searchParams;
+"use strict";
 let preview = document.getElementById("screenshot-preview");
-let url = document.getElementById("url");
-url.innerText = params.get("url");
-let details = document.getElementById("details");
-details.innerText = params.get("details");
 
 function getBlobAsDataURL(blob) {
   return new Promise((resolve, reject) => {
     let reader = new FileReader();
 
+    // eslint-disable-next-line mozilla/balanced-listeners
     reader.addEventListener("error", (e) => {
       reject(`There was an error reading the blob: ${e.type}`);
     });
 
+    // eslint-disable-next-line mozilla/balanced-listeners
     reader.addEventListener("load", (e) => {
       resolve(e.target.result);
     });
 
     reader.readAsDataURL(blob);
   });
 }
 
@@ -36,16 +34,17 @@ function setPreviewBG(backgroundData) {
     resolve();
   });
 }
 
 function sendReceivedEvent() {
   window.dispatchEvent(new CustomEvent("ScreenshotReceived", {bubbles: true}));
 }
 
+// eslint-disable-next-line mozilla/balanced-listeners
 window.addEventListener("message", function(event) {
   if (event.data instanceof Blob) {
     preview.innerText = "Pass";
   }
 
   getBlobAsDataURL(event.data).then(setPreviewBG).then(sendReceivedEvent);
 });
 </script>
--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -1040,17 +1040,17 @@ Action.prototype = {
     let builtInIDs = [
       "pocket",
       "screenshots_mozilla_org",
     ].concat(gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id));
     return builtInIDs.includes(this.id);
   },
 
   get _isMozillaAction() {
-    return this._isBuiltIn || this.id == "webcompat-reporter-button";
+    return this._isBuiltIn || this.id == "webcompat-reporter_mozilla_org";
   },
 };
 
 this.PageActions.Action = Action;
 
 this.PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
 this.PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
 
--- a/browser/modules/test/browser/head.js
+++ b/browser/modules/test/browser/head.js
@@ -274,11 +274,11 @@ function getPopupNotificationNode() {
 
 /**
  * Disable non-release page actions (that are tested elsewhere).
  *
  * @return void
  */
 async function disableNonReleaseActions() {
   if (AppConstants.MOZ_DEV_EDITION || AppConstants.NIGHTLY_BUILD) {
-    await SpecialPowers.pushPrefEnv({set: [["extensions.webcompat-reporter.enabled", false]]});
+    SpecialPowers.Services.prefs.setBoolPref("extensions.webcompat-reporter.enabled", false);
   }
 }