Bug 1324062. Part 1 - Add a "Report Site Issue" button to the panel menu for NIGHTLY_BUILDs. r=Gijs
authorMike Taylor <miket@mozilla.com>
Tue, 17 Jan 2017 14:27:03 -0600
changeset 330362 cf749e7246d71a5b31d93cca2e98bb4c91eb6b41
parent 330361 68abeaaf3a2e1c4f32e618de595006b4c65a15ca
child 330363 8debe3af85150bfceb5c19e9c7627eb309604318
push id36319
push usermitaylor@mozilla.com
push dateFri, 20 Jan 2017 23:47:39 +0000
treeherderautoland@6ee916b9fa07 [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
@@ -22,8 +22,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
@@ -4735,16 +4735,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);