Bug 1324062. Part 1 - Add a "Report Site Issue" button to the panel menu for NIGHTLY_BUILDs. r=Gijs draft
authorMike Taylor <miket@mozilla.com>
Tue, 17 Jan 2017 14:27:03 -0600
changeset 463932 2799df2945b71ac5ef902bf69603cec2c63b458a
parent 463890 a8d2fc52fdeec706da43fd2ae824a03a462fce82
child 463933 0c622e5da02fe2ce50557ac89baf6822bbbb492f
push id42230
push userbmo:miket@mozilla.com
push dateFri, 20 Jan 2017 01:50:19 +0000
reviewersGijs
bugs1324062
milestone53.0a1
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 not CONFIG['RELEASE_OR_BETA']:
         '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,163 @@
+/* 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");
+});
+
+XPCOMUtils.defineLazyGetter(this, "wcStyleURI", function() {
+  return Services.io.newURI("chrome://webcompat-reporter/skin/lightbulb.css");
+});
+
+const WIDGET_ID = "webcompat-reporter-button";
+const TABLISTENER_JSM = "chrome://webcompat-reporter/content/TabListener.jsm";
+
+let WebCompatReporter = {
+  get endpoint() {
+    return Services.urlFormatter.formatURLPref(
+      "extensions.webcompat-reporter.newIssueEndpoint");
+  },
+
+  init() {
+    Cu.import(TABLISTENER_JSM);
+
+    let styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
+      .getService(Ci.nsIStyleSheetService);
+    this._sheetType = styleSheetService.AUTHOR_SHEET;
+    this._cachedSheet = styleSheetService.preloadSheet(wcStyleURI,
+                                                       this._sheetType);
+
+    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.
+    win.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindowUtils)
+      .addSheet(this._cachedSheet, this._sheetType);
+    // 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);
+
+      win.QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIDOMWindowUtils)
+        .removeSheet(wcStyleURI, this._sheetType);
+    }
+
+    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,48 @@
+<!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");
+
+function getBlobAsDataURL(blob) {
+  return new Promise((resolve, reject) => {
+    let reader = new FileReader();
+
+    reader.addEventListener("error", (e) => {
+      reject(`There was an error reading the blob: ${e.type}`);
+    });
+
+    reader.addEventListener("load", (e) => {
+      resolve(e.target.result);
+    });
+
+    reader.readAsDataURL(blob);
+  });
+}
+
+function setPreviewBG(backgroundData) {
+  return new Promise((resolve) => {
+    preview.style.background = `url(${backgroundData})`;
+    resolve();
+  });
+}
+
+function sendReceivedEvent() {
+  window.dispatchEvent(new CustomEvent("ScreenshotReceived", {bubbles:true}));
+}
+
+window.addEventListener("message", function(event) {
+  if (event.data instanceof Blob) {
+    preview.innerText = "Pass";
+  }
+
+  getBlobAsDataURL(event.data).then(setPreviewBG).then(sendReceivedEvent);
+});
+</script>
\ No newline at end of file
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -101,16 +101,19 @@ libs-%:
 ifndef RELEASE_OR_BETA
 	@$(MAKE) -C ../extensions/presentation/locale AB_CD=$* XPI_NAME=locale-$*
 endif
 	@$(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
@@ -4740,16 +4740,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);