Bug 1324062. Part 1 - Add a "Report Site Issue" button to the panel menu for NIGHTLY_BUILDs. r=Gijs
☠☠ backed out by 879c999acb1d ☠ ☠
authorMike Taylor <miket@mozilla.com>
Wed, 28 Dec 2016 13:31:22 -0600
changeset 374076 028db619f166de461a43022538a98070f7f7db46
parent 374075 6bf8894b084d3df108f662464a4ac3677c45f9a4
child 374077 1df935d51464609e8aa85c3db86ed12f2fdf0aa6
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1324062
milestone53.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 1324062. Part 1 - Add a "Report Site Issue" button to the panel menu for NIGHTLY_BUILDs. r=Gijs MozReview-Commit-ID: 74T8uRuqpyf
browser/components/customizableui/CustomizableUI.jsm
browser/extensions/moz.build
browser/extensions/webcompat-reporter/bootstrap.js
browser/extensions/webcompat-reporter/content/TabListener.jsm
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/install.rdf.in
browser/extensions/webcompat-reporter/jar.mn
browser/extensions/webcompat-reporter/locale/en-US/webcompat.properties
browser/extensions/webcompat-reporter/locale/jar.mn
browser/extensions/webcompat-reporter/locale/moz.build
browser/extensions/webcompat-reporter/moz.build
browser/extensions/webcompat-reporter/skin/lightbulb.css
browser/extensions/webcompat-reporter/skin/lightbulb.svg
browser/extensions/webcompat-reporter/test/browser/.eslintrc.js
browser/extensions/webcompat-reporter/test/browser/browser.ini
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/test.html
browser/extensions/webcompat-reporter/test/browser/webcompat.html
browser/locales/Makefile.in
browser/modules/BrowserUITelemetry.jsm
modules/libpref/init/all.js
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -220,16 +220,22 @@ var CustomizableUIInternal = {
     let showCharacterEncoding = Services.prefs.getComplexValue(
       "browser.menu.showCharacterEncoding",
       Ci.nsIPrefLocalizedString
     ).data;
     if (showCharacterEncoding == "true") {
       panelPlacements.push("characterencoding-button");
     }
 
+    if (AppConstants.NIGHTLY_BUILD) {
+      if (Services.prefs.getBoolPref("extensions.webcompat-reporter.enabled")) {
+        panelPlacements.push("webcompat-reporter-button");
+      }
+    }
+
     this.registerArea(CustomizableUI.AREA_PANEL, {
       anchor: "PanelUI-menu-button",
       type: CustomizableUI.TYPE_MENU_PANEL,
       defaultPlacements: panelPlacements
     }, true);
     PanelWideWidgetTracker.init();
 
     let navbarPlacements = [
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -21,8 +21,14 @@ if 'a' in CONFIG['GRE_MILESTONE']:
         'presentation',
     ]
 
 # Only include mortar system add-ons if we locally enable it
 if CONFIG['MOZ_MORTAR']:
     DIRS += [
         'mortar',
     ]
+
+# Nightly-only system add-ons
+if CONFIG['NIGHTLY_BUILD']:
+    DIRS += [
+        'webcompat-reporter',
+    ]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/bootstrap.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const WEBCOMPATREPORTER_JSM = "chrome://webcompat-reporter/content/WebCompatReporter.jsm";
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebCompatReporter",
+  WEBCOMPATREPORTER_JSM);
+
+const PREF_WC_REPORTER_ENABLED = "extensions.webcompat-reporter.enabled";
+
+let prefObserver = function(aSubject, aTopic, aData) {
+  let enabled = Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED);
+  if (enabled) {
+    WebCompatReporter.init();
+  } else {
+    WebCompatReporter.uninit();
+  }
+};
+
+function startup(aData, aReason) {
+  // Observe pref changes and enable/disable as necessary.
+  Services.prefs.addObserver(PREF_WC_REPORTER_ENABLED, prefObserver, false);
+
+  // Only initialize if pref is enabled.
+  let enabled = Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED);
+  if (enabled) {
+    WebCompatReporter.init();
+  }
+}
+
+function shutdown(aData, aReason) {
+  if (aReason === APP_SHUTDOWN) {
+    return;
+  }
+
+  Cu.unload(WEBCOMPATREPORTER_JSM);
+}
+
+function install(aData, aReason) {}
+function uninstall(aData, aReason) {}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/TabListener.jsm
@@ -0,0 +1,63 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["TabListener"];
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+ "resource:///modules/CustomizableUI.jsm");
+
+const WIDGET_ID = "webcompat-reporter-button";
+
+// Class that watches for url/location/tab changes and enables or disables
+// the Report Site Issue button accordingly
+class TabListener {
+  constructor(win) {
+    this.win = win;
+    this.browser = win.gBrowser;
+    this.addListeners();
+  }
+
+  addListeners() {
+    this.browser.addTabsProgressListener(this);
+    this.browser.tabContainer.addEventListener("TabSelect", this);
+  }
+
+  removeListeners() {
+    this.browser.removeTabsProgressListener(this);
+    this.browser.tabContainer.removeEventListener("TabSelect", this);
+  }
+
+  handleEvent(e) {
+    switch (e.type) {
+      case "TabSelect":
+        this.setButtonState(e.target.linkedBrowser.currentURI.scheme);
+        break;
+    }
+  }
+
+  onLocationChange(browser, webProgress, request, uri, flags) {
+    this.setButtonState(uri.scheme);
+  }
+
+  static isReportableScheme(scheme) {
+    return ["http", "https"].some((prefix) => scheme.startsWith(prefix));
+  }
+
+  setButtonState(scheme) {
+    // Bail early if the button is in the palette.
+    if (!CustomizableUI.getPlacementOfWidget(WIDGET_ID)) {
+      return;
+    }
+
+    if (TabListener.isReportableScheme(scheme)) {
+      CustomizableUI.getWidget(WIDGET_ID).forWindow(this.win).node.removeAttribute("disabled");
+    } else {
+      CustomizableUI.getWidget(WIDGET_ID).forWindow(this.win).node.setAttribute("disabled", true);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/WebCompatReporter.jsm
@@ -0,0 +1,162 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["WebCompatReporter"];
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+  "resource:///modules/CustomizableUI.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "wcStrings", function() {
+  return Services.strings.createBundle(
+    "chrome://webcompat-reporter/locale/webcompat.properties");
+});
+
+const WIDGET_ID = "webcompat-reporter-button";
+const TABLISTENER_JSM = "chrome://webcompat-reporter/content/TabListener.jsm";
+const LIGHTBULB_CSS = "chrome://webcompat-reporter/skin/lightbulb.css";
+
+// Weak mapping between windows and stylesheet nodes, so we can remove
+// them if the button is disabled.
+let styleSheetRefs = new WeakMap();
+
+let WebCompatReporter = {
+  get endpoint() {
+    return Services.urlFormatter.formatURLPref(
+      "extensions.webcompat-reporter.newIssueEndpoint");
+  },
+
+  init() {
+    Cu.import(TABLISTENER_JSM);
+
+    CustomizableUI.createWidget({
+      id: WIDGET_ID,
+      label: wcStrings.GetStringFromName("wc-reporter.label"),
+      tooltiptext: wcStrings.GetStringFromName("wc-reporter.tooltip"),
+      defaultArea: CustomizableUI.AREA_PANEL,
+      disabled: true,
+      onCommand: (e) => this.reportIssue(e.target.ownerDocument),
+    });
+
+    for (let win of CustomizableUI.windows) {
+      this.onWindowOpened(win);
+    }
+
+    CustomizableUI.addListener(this);
+  },
+
+  onWindowOpened(win) {
+      // Attach stylesheet for the button icon.
+      let data = `type="text/css" href="${LIGHTBULB_CSS}"`;
+      let stylesheet = win.document.createProcessingInstruction(
+        "xml-stylesheet", data);
+      win.document.insertBefore(stylesheet, win.document.documentElement);
+      styleSheetRefs.set(win, stylesheet);
+      // Attach listeners to new window.
+      win._webcompatReporterTabListener = new TabListener(win);
+  },
+
+  onWindowClosed(win) {
+    if (win._webcompatReporterTabListener) {
+      win._webcompatReporterTabListener.removeListeners();
+      delete win._webcompatReporterTabListener;
+    }
+  },
+
+  uninit() {
+    CustomizableUI.destroyWidget(WIDGET_ID);
+
+    for (let win of CustomizableUI.windows) {
+      this.onWindowClosed(win);
+
+      let stylesheet = styleSheetRefs.get(win);
+      if (stylesheet) {
+        stylesheet.remove();
+      }
+      styleSheetRefs.delete(win);
+    }
+
+    CustomizableUI.removeListener(this);
+    Cu.unload(TABLISTENER_JSM);
+  },
+
+  // This method injects a framescript that should send back a screenshot blob
+  // of the top-level window of the currently selected tab, 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;
+
+    let tab = gBrowser.loadOneTab(
+      `${WebCompatReporter.endpoint}?url=${encodeURIComponent(tabData.url)}&src=desktop-reporter`,
+      {inBackground: false});
+
+    // 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: XPCOMUtils.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(xulDoc) {
+    this.getScreenshot(xulDoc.defaultView.gBrowser).then(this.openWebCompatTab)
+                                                   .catch(Cu.reportError);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/tab-frame.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/. */
+
+let { utils: Cu } = Components;
+
+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({url, blob});
+      });
+    } 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));
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/wc-frame.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+let { utils: Cu } = Components;
+
+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/install.rdf.in
@@ -0,0 +1,29 @@
+<?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>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/jar.mn
@@ -0,0 +1,9 @@
+# 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/
+% skin webcompat-reporter classic/1.0 %skin/
+  content/  (content/*)
+  skin/  (skin/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/locale/en-US/webcompat.properties
@@ -0,0 +1,2 @@
+wc-reporter.label=Report Site Issue
+wc-reporter.tooltip=Report a site compatibility issue
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/locale/jar.mn
@@ -0,0 +1,8 @@
+#filter substitution
+# 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] @AB_CD@.jar:
+% locale webcompat-reporter @AB_CD@ %locale/@AB_CD@/
+  locale/@AB_CD@/                    (en-US/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/locale/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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']
+
+DIRS += ['locale']
+
+FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'] += [
+  'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['webcompat-reporter@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/skin/lightbulb.css
@@ -0,0 +1,6 @@
+/* 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/. */
+#webcompat-reporter-button {
+  list-style-image: url("chrome://webcompat-reporter/skin/lightbulb.svg");
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/skin/lightbulb.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 85 128">
+  <path d="M85.23,34.52v-7L43.34,35.21v6.94Zm0-16L43.34,26.28v6.94L85.23,25.5Zm-2.86-1.47v-.26A15,15,0,0,0,81.58,12h0a1.23,1.23,0,0,1-.17-.52,3.62,3.62,0,0,0-.35-.87.3.3,0,0,0-.09-.17h0A18.33,18.33,0,0,0,64.15,0C55.91,0,49,5,46.72,12h25.5L45.94,16.82l-2.6.43v7l4.08-.78Z" transform="translate(-23.46)" fill-opacity="0.6"/>
+  <path d="M28.65,92.41c1.7,18,14.52,30.59,35.83,30.59s34.13-12.61,35.83-30.59c1.33-18.21-4.72-23.88-12.75-35.61a36.8,36.8,0,0,1-5.23-11.57s-24.25.29-24.33,0H46.71A36.8,36.8,0,0,1,41.47,56.8C33.36,68.52,27.32,74.2,28.65,92.41Z" transform="translate(-23.46)" fill="none" stroke="#000" stroke-opacity="0.6" stroke-width="10"/>
+  <path d="M82.11,86.19c-.17-.26-.35-.43-.61-2L70.83,51.58c-1.13.17-.69.17-1.13.17h-.78c-.52.17-.35.78-.17,1.3L78.81,83.67a11.18,11.18,0,0,0-8-1,7.69,7.69,0,0,0-1.47.69l-4.25-1.3a8.07,8.07,0,0,0-4.94,1.65,11.54,11.54,0,0,0-2.6-1,10.73,10.73,0,0,0-7.46.52L60,53.14a3.83,3.83,0,0,0,.26-1.39c-2.25-.09-2.25.09-2.17.69L47.24,85.58a1.2,1.2,0,0,0-.26.35,1,1,0,1,0,1.56,1.21c2-2.52,4.86-3.3,8.5-2.34a5.55,5.55,0,0,1,1.65.61,5.19,5.19,0,0,0-.87,3.12c.09,2.25.87,3.82,2.08,4.42a2.16,2.16,0,0,0,2.17-.09c1-.69,1.56-2.34,1.39-4.42a5.24,5.24,0,0,0-1.65-3.38,6.35,6.35,0,0,1,6-.17,6.65,6.65,0,0,0-1.13,3.56c-.09,2,.52,3.56,1.65,4.25a2.48,2.48,0,0,0,2.34.09c1.13-.69,1.82-2.25,1.73-4.25A5.78,5.78,0,0,0,71,84.88c.17-.09.26-.09.43-.17,3-1,7.46.78,9.11,2.78a1,1,0,0,0,1.39.17A1.09,1.09,0,0,0,82.11,86.19ZM60.86,91.13h-.09a3.58,3.58,0,0,1-1-2.69,4,4,0,0,1,.52-2,3,3,0,0,1,1,2.08C61.55,90.09,61.2,91,60.86,91.13ZM69.62,91h-.09a.27.27,0,0,1-.17-.09c-.26-.17-.78-.87-.69-2.43A5.31,5.31,0,0,1,69.36,86a3.51,3.51,0,0,1,1,2.34C70.31,90.09,69.88,90.78,69.62,91Z" transform="translate(-23.46)" fill="none" stroke="#000" stroke-opacity="0.6" stroke-width="5"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "../../../../../testing/mochitest/browser.eslintrc.js"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+  head.js
+  test.html
+  webcompat.html
+
+[browser_disabled_cleanup.js]
+[browser_button_state.js]
+[browser_report_site_issue.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
@@ -0,0 +1,28 @@
+const REPORTABLE_PAGE = "http://example.com/";
+const REPORTABLE_PAGE2 = "https://example.com/";
+const NONREPORTABLE_PAGE = "about:blank";
+
+/* Test that the Report Site Issue button is enabled for http and https tabs,
+   on page load, or TabSelect, and disabled for everything else. */
+add_task(function* test_button_state_disabled() {
+  let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE);
+  yield PanelUI.show();
+  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
+
+  let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, NONREPORTABLE_PAGE);
+  is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on tab load");
+
+  let tab3 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE2);
+  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
+
+
+  yield BrowserTestUtils.switchTab(gBrowser, tab2);
+  is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on TabSelect");
+
+  yield BrowserTestUtils.switchTab(gBrowser, tab1);
+  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on TabSelect");
+
+  yield BrowserTestUtils.removeTab(tab1);
+  yield BrowserTestUtils.removeTab(tab2);
+  yield BrowserTestUtils.removeTab(tab3);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_disabled_cleanup.js
@@ -0,0 +1,9 @@
+// Test the addon is cleaning up after itself when disabled.
+add_task(function* test_disabled() {
+  yield SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENABLED, false]]});
+
+  yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function() {
+    is(typeof window._webCompatReporterTabListener, "undefined", "TabListener expando does not exist.");
+    is(document.getElementById("webcompat-reporter-button"), null, "Report Site Issue button does not exist.");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
@@ -0,0 +1,32 @@
+/* Test that clicking on the Report Site Issue button opens a new tab
+   and sends a postMessaged blob at it.
+   testing/profiles/prefs_general.js sets the value for
+   "extensions.webcompat-reporter.newIssueEndpoint" */
+add_task(function* test_screenshot() {
+  yield SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENDPOINT, NEW_ISSUE_PAGE]]});
+
+  let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+  yield PanelUI.show();
+
+  let webcompatButton = document.getElementById("webcompat-reporter-button");
+  ok(webcompatButton, "Report Site Issue button exists.");
+
+  let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+  webcompatButton.click();
+  let tab2 = yield newTabPromise;
+
+  yield BrowserTestUtils.waitForContentEvent(tab2.linkedBrowser, "ScreenshotReceived", false, null, true);
+
+  yield ContentTask.spawn(tab2.linkedBrowser, {TEST_PAGE}, function(args) {
+    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");
+
+    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");
+  });
+
+  yield BrowserTestUtils.removeTab(tab2);
+  yield BrowserTestUtils.removeTab(tab1);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/head.js
@@ -0,0 +1,10 @@
+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";
+
+function isButtonDisabled() {
+  return document.getElementById("webcompat-reporter-button").disabled;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/test.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  body {background: rgb(0, 128, 0);}
+</style>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/webcompat.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+ #screenshot-preview {width: 200px; height: 200px;}
+</style>
+<div id="url"></div>
+<div id="screenshot-preview">Fail</div>
+<script>
+let params = new URL(location.href).searchParams;
+let preview = document.getElementById("screenshot-preview");
+let url = document.getElementById("url");
+url.innerText = params.get("url");
+
+window.addEventListener("message", function(event) {
+  if (event.data instanceof Blob) {
+    preview.innerText = "Pass";
+  }
+
+  let reader = new FileReader();
+  reader.addEventListener("load", (e) => {
+    preview.style.background = `url(${e.target.result})`;
+    window.dispatchEvent(new CustomEvent("ScreenshotReceived", {bubbles:true}));
+  });
+  reader.readAsDataURL(event.data);
+});
+</script>
\ No newline at end of file
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -99,16 +99,19 @@ libs-%:
 	@$(MAKE) -C ../../extensions/spellcheck/locales AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../extensions/pocket/locale AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../extensions/presentation/locale AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../../intl/locales AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../../devtools/client/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
 	@$(MAKE) -B searchplugins AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=$(PREF_DIR)
 	@$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales AB_CD=$* XPI_NAME=locale-$*
+ifdef NIGHTLY_BUILD
+	@$(MAKE) -C ../extensions/webcompat-reporter/locale AB_CD=$* XPI_NAME=locale-$*
+endif
 
 repackage-win32-installer: WIN32_INSTALLER_OUT=$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe
 repackage-win32-installer: $(call ESCAPE_WILDCARD,$(WIN32_INSTALLER_IN)) $(SUBMAKEFILES) libs-$(AB_CD)
 	@echo 'Repackaging $(WIN32_INSTALLER_IN) into $(WIN32_INSTALLER_OUT).'
 	$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY) export
 	$(MAKE) -C ../installer/windows CONFIG_DIR=l10ngen l10ngen/setup.exe l10ngen/7zSD.sfx
 	$(MAKE) repackage-zip \
 	  AB_CD=$(AB_CD) \
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -6,16 +6,18 @@
 
 this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"];
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+  "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
   "resource:///modules/UITour.jsm");
@@ -74,16 +76,22 @@ XPCOMUtils.defineLazyGetter(this, "DEFAU
   let showCharacterEncoding = Services.prefs.getComplexValue(
     "browser.menu.showCharacterEncoding",
     Ci.nsIPrefLocalizedString
   ).data;
   if (showCharacterEncoding == "true") {
     result["PanelUI-contents"].push("characterencoding-button");
   }
 
+  if (AppConstants.NIGHTLY_BUILD) {
+    if (Services.prefs.getBoolPref("extensions.webcompat-reporter.enabled")) {
+      result["PanelUI-contents"].push("webcompat-reporter-button");
+    }
+  }
+
   return result;
 });
 
 XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() {
   return Object.keys(DEFAULT_AREA_PLACEMENTS);
 });
 
 XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4732,16 +4732,24 @@ pref("extensions.webExtensionsMinPlatfor
 
 // Other webextensions prefs
 pref("extensions.webextensions.keepStorageOnUninstall", false);
 pref("extensions.webextensions.keepUuidOnUninstall", false);
 // Redirect basedomain used by identity api
 pref("extensions.webextensions.identity.redirectDomain", "extensions.allizom.org");
 pref("extensions.webextensions.remote", false);
 
+// Report Site Issue button
+pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/issues/new");
+#ifdef NIGHTLY_BUILD
+pref("extensions.webcompat-reporter.enabled", true);
+#else
+pref("extensions.webcompat-reporter.enabled", false);
+#endif
+
 pref("network.buffer.cache.count", 24);
 pref("network.buffer.cache.size",  32768);
 
 // Desktop Notification
 pref("notification.feature.enabled", false);
 
 // Web Notification
 pref("dom.webnotifications.enabled", true);