summaryrefslogtreecommitdiffstats
path: root/browser/extensions/report-site-issue
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/report-site-issue')
-rw-r--r--browser/extensions/report-site-issue/.eslintrc.js76
-rw-r--r--browser/extensions/report-site-issue/background.js215
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js41
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json35
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm156
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js74
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json57
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/l10n.js57
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/l10n.json19
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js43
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json41
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js97
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json19
-rw-r--r--browser/extensions/report-site-issue/icons/lightbulb.svg6
-rw-r--r--browser/extensions/report-site-issue/locales/en-US/webcompat.properties10
-rw-r--r--browser/extensions/report-site-issue/locales/jar.mn8
-rw-r--r--browser/extensions/report-site-issue/locales/moz.build7
-rw-r--r--browser/extensions/report-site-issue/manifest.json78
-rw-r--r--browser/extensions/report-site-issue/moz.build41
-rw-r--r--browser/extensions/report-site-issue/test/browser/.eslintrc.js5
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser.ini11
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_button_state.js93
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js53
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js291
-rw-r--r--browser/extensions/report-site-issue/test/browser/fastclick.html11
-rw-r--r--browser/extensions/report-site-issue/test/browser/frameworks.html8
-rw-r--r--browser/extensions/report-site-issue/test/browser/head.js204
-rw-r--r--browser/extensions/report-site-issue/test/browser/test.html39
-rw-r--r--browser/extensions/report-site-issue/test/browser/webcompat.html85
29 files changed, 1880 insertions, 0 deletions
diff --git a/browser/extensions/report-site-issue/.eslintrc.js b/browser/extensions/report-site-issue/.eslintrc.js
new file mode 100644
index 0000000000..e26bd6da3f
--- /dev/null
+++ b/browser/extensions/report-site-issue/.eslintrc.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ rules: {
+ // Rules from the mozilla plugin
+ "mozilla/balanced-listeners": "error",
+ "mozilla/no-aArgs": "error",
+ "mozilla/var-only-at-top-level": "error",
+
+ "valid-jsdoc": [
+ "error",
+ {
+ prefer: {
+ return: "returns",
+ },
+ preferType: {
+ Boolean: "boolean",
+ Number: "number",
+ String: "string",
+ bool: "boolean",
+ },
+ requireParamDescription: false,
+ requireReturn: false,
+ requireReturnDescription: false,
+ },
+ ],
+
+ // No expressions where a statement is expected
+ "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": "error",
+
+ // Warn about cyclomatic complexity in functions.
+ complexity: ["error", { max: 26 }],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 4],
+
+ // Allow the console API aside from console.log.
+ "no-console": ["error", { allow: ["error", "info", "trace", "warn"] }],
+
+ // Disallow fallthrough of case statements, except if there is a comment.
+ "no-fallthrough": "error",
+
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": "error",
+
+ // Disallow usage of __proto__ property.
+ "no-proto": "error",
+
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": "error",
+
+ // Require use of the second argument for parseInt().
+ radix: "error",
+
+ // Require "use strict" to be defined globally in the script.
+ strict: ["error", "global"],
+
+ // Disallow Yoda conditions (where literal value comes first).
+ yoda: "error",
+
+ // Disallow function or variable declarations in nested blocks
+ "no-inner-declarations": "error",
+ },
+};
diff --git a/browser/extensions/report-site-issue/background.js b/browser/extensions/report-site-issue/background.js
new file mode 100644
index 0000000000..d5c4edcbd7
--- /dev/null
+++ b/browser/extensions/report-site-issue/background.js
@@ -0,0 +1,215 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* globals browser */
+
+const Config = {
+ newIssueEndpoint: "https://webcompat.com/issues/new",
+ newIssueEndpointPref: "newIssueEndpoint",
+ screenshotFormat: {
+ format: "jpeg",
+ quality: 75,
+ },
+};
+
+const FRAMEWORK_KEYS = ["hasFastClick", "hasMobify", "hasMarfeel"];
+
+browser.pageActionExtras.setLabelForHistogram("webcompat");
+
+browser.pageAction.onClicked.addListener(tab => {
+ return getWebCompatInfoForTab(tab).then(
+ info => {
+ return openWebCompatTab(info);
+ },
+ err => {
+ console.error("WebCompat Reporter: unexpected error", err);
+ }
+ );
+});
+
+browser.aboutConfigPrefs.onEndpointPrefChange.addListener(checkEndpointPref);
+
+checkEndpointPref();
+
+async function checkEndpointPref() {
+ const value = await browser.aboutConfigPrefs.getEndpointPref();
+ if (value === undefined) {
+ browser.aboutConfigPrefs.setEndpointPref(Config.newIssueEndpoint);
+ } else {
+ Config.newIssueEndpoint = value;
+ }
+}
+
+function hasFastClickPageScript() {
+ const win = window.wrappedJSObject;
+
+ if (win.FastClick) {
+ return true;
+ }
+
+ for (const property in win) {
+ try {
+ const proto = win[property].prototype;
+ if (proto && proto.needsClick) {
+ return true;
+ }
+ } catch (_) {}
+ }
+
+ return false;
+}
+
+function hasMobifyPageScript() {
+ const win = window.wrappedJSObject;
+ return !!(win.Mobify && win.Mobify.Tag);
+}
+
+function hasMarfeelPageScript() {
+ const win = window.wrappedJSObject;
+ return !!win.marfeel;
+}
+
+function checkForFrameworks(tabId) {
+ return browser.tabs
+ .executeScript(tabId, {
+ code: `
+ (function() {
+ ${hasFastClickPageScript};
+ ${hasMobifyPageScript};
+ ${hasMarfeelPageScript};
+
+ const result = {
+ hasFastClick: hasFastClickPageScript(),
+ hasMobify: hasMobifyPageScript(),
+ hasMarfeel: hasMarfeelPageScript(),
+ }
+
+ return result;
+ })();
+ `,
+ })
+ .then(([results]) => results)
+ .catch(() => false);
+}
+
+function getWebCompatInfoForTab(tab) {
+ const { id, url } = tab;
+ return Promise.all([
+ browser.browserInfo.getBlockList(),
+ browser.browserInfo.getBuildID(),
+ browser.browserInfo.getGraphicsPrefs(),
+ browser.browserInfo.getUpdateChannel(),
+ browser.browserInfo.hasTouchScreen(),
+ browser.tabExtras.getWebcompatInfo(id),
+ checkForFrameworks(id),
+ browser.tabs.captureTab(id, Config.screenshotFormat).catch(e => {
+ console.error("WebCompat Reporter: getting a screenshot failed", e);
+ return Promise.resolve(undefined);
+ }),
+ ]).then(
+ ([
+ blockList,
+ buildID,
+ graphicsPrefs,
+ channel,
+ hasTouchScreen,
+ frameInfo,
+ frameworks,
+ screenshot,
+ ]) => {
+ if (channel !== "linux") {
+ delete graphicsPrefs["layers.acceleration.force-enabled"];
+ }
+
+ const consoleLog = frameInfo.log;
+ delete frameInfo.log;
+
+ return Object.assign(frameInfo, {
+ tabId: id,
+ blockList,
+ details: Object.assign(graphicsPrefs, {
+ buildID,
+ channel,
+ consoleLog,
+ frameworks,
+ hasTouchScreen,
+ "mixed active content blocked":
+ frameInfo.hasMixedActiveContentBlocked,
+ "mixed passive content blocked":
+ frameInfo.hasMixedDisplayContentBlocked,
+ "tracking content blocked": frameInfo.hasTrackingContentBlocked
+ ? `true (${blockList})`
+ : "false",
+ }),
+ screenshot,
+ url,
+ });
+ }
+ );
+}
+
+function stripNonASCIIChars(str) {
+ // eslint-disable-next-line no-control-regex
+ return str.replace(/[^\x00-\x7F]/g, "");
+}
+
+browser.l10n
+ .getMessage("wc-reporter.label2")
+ .then(browser.pageActionExtras.setDefaultTitle, () => {});
+
+browser.l10n
+ .getMessage("wc-reporter.tooltip")
+ .then(browser.pageActionExtras.setTooltipText, () => {});
+
+async function openWebCompatTab(compatInfo) {
+ const url = new URL(Config.newIssueEndpoint);
+ const { details } = compatInfo;
+ const params = {
+ url: `${compatInfo.url}`,
+ utm_source: "desktop-reporter",
+ utm_campaign: "report-site-issue-button",
+ src: "desktop-reporter",
+ details,
+ extra_labels: [],
+ };
+
+ for (let framework of FRAMEWORK_KEYS) {
+ if (details.frameworks[framework]) {
+ params.details[framework] = true;
+ params.extra_labels.push(
+ framework.replace(/^has/, "type-").toLowerCase()
+ );
+ }
+ }
+ delete details.frameworks;
+
+ if (details["gfx.webrender.all"] || details["gfx.webrender.enabled"]) {
+ params.extra_labels.push("type-webrender-enabled");
+ }
+ if (compatInfo.hasTrackingContentBlocked) {
+ params.extra_labels.push(
+ `type-tracking-protection-${compatInfo.blockList}`
+ );
+ }
+
+ const json = stripNonASCIIChars(JSON.stringify(params));
+ const tab = await browser.tabs.create({ url: url.href });
+ await browser.tabs.executeScript(tab.id, {
+ runAt: "document_end",
+ code: `(function() {
+ async function postMessageData(dataURI, metadata) {
+ const res = await fetch(dataURI);
+ const blob = await res.blob();
+ const data = {
+ screenshot: blob,
+ message: metadata
+ };
+ postMessage(data, "${url.origin}");
+ }
+ postMessageData("${compatInfo.screenshot}", ${json});
+ })()`,
+ });
+}
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js
new file mode 100644
index 0000000000..97206ba754
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, ExtensionCommon */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+ getAPI(context) {
+ const EventManager = ExtensionCommon.EventManager;
+ const extensionIDBase = context.extension.id.split("@")[0];
+ const endpointPrefName = `extensions.${extensionIDBase}.newIssueEndpoint`;
+
+ return {
+ aboutConfigPrefs: {
+ onEndpointPrefChange: new EventManager({
+ context,
+ name: "aboutConfigPrefs.onEndpointPrefChange",
+ register: fire => {
+ const callback = () => {
+ fire.async().catch(() => {}); // ignore Message Manager disconnects
+ };
+ Services.prefs.addObserver(endpointPrefName, callback);
+ return () => {
+ Services.prefs.removeObserver(endpointPrefName, callback);
+ };
+ },
+ }).api(),
+ async getEndpointPref() {
+ return Services.prefs.getStringPref(endpointPrefName, undefined);
+ },
+ async setEndpointPref(value) {
+ Services.prefs.setStringPref(endpointPrefName, value);
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json
new file mode 100644
index 0000000000..1fd313e392
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json
@@ -0,0 +1,35 @@
+[
+ {
+ "namespace": "aboutConfigPrefs",
+ "description": "experimental API extension to allow access to about:config preferences",
+ "events": [
+ {
+ "name": "onEndpointPrefChange",
+ "type": "function",
+ "parameters": []
+ }
+ ],
+ "functions": [
+ {
+ "name": "getEndpointPref",
+ "type": "function",
+ "description": "Get the endpoint preference's value",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "setEndpointPref",
+ "type": "function",
+ "description": "Set the endpoint preference's value",
+ "parameters": [
+ {
+ "name": "value",
+ "type": "string",
+ "description": "The new value"
+ }
+ ],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm b/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm
new file mode 100644
index 0000000000..869856dbea
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/actors/tabExtrasActor.jsm
@@ -0,0 +1,156 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var EXPORTED_SYMBOLS = ["ReportSiteIssueHelperChild"];
+
+const PREVIEW_MAX_ITEMS = 10;
+const LOG_LEVELS = ["debug", "info", "warn", "error"];
+
+function getPreview(value) {
+ switch (typeof value) {
+ case "function":
+ return "function ()";
+
+ case "object":
+ if (value === null) {
+ return null;
+ }
+
+ if (Array.isArray(value)) {
+ return `(${value.length})[...]`;
+ }
+
+ return "{...}";
+
+ case "undefined":
+ return "undefined";
+
+ default:
+ return value;
+ }
+}
+
+function getArrayPreview(arr) {
+ const preview = [];
+ let count = 0;
+ for (const value of arr) {
+ if (++count > PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ preview.push(getPreview(value));
+ }
+
+ return preview;
+}
+
+function getObjectPreview(obj) {
+ const preview = {};
+ let count = 0;
+ for (const key of Object.keys(obj)) {
+ if (++count > PREVIEW_MAX_ITEMS) {
+ break;
+ }
+ preview[key] = getPreview(obj[key]);
+ }
+
+ return preview;
+}
+
+function getArgs(value) {
+ if (typeof value === "object" && value !== null) {
+ if (Array.isArray(value)) {
+ return getArrayPreview(value);
+ }
+
+ return getObjectPreview(value);
+ }
+
+ return getPreview(value);
+}
+
+class ReportSiteIssueHelperChild extends JSWindowActorChild {
+ _getConsoleMessages(windowId) {
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+ let messages = ConsoleAPIStorage.getEvents(windowId);
+ return messages.map(evt => {
+ const { columnNumber, filename, level, lineNumber, timeStamp } = evt;
+ const args = evt.arguments.map(getArgs);
+
+ const message = {
+ level,
+ log: args,
+ uri: filename,
+ pos: `${lineNumber}:${columnNumber}`,
+ };
+
+ return { timeStamp, message };
+ });
+ }
+
+ _getScriptErrors(windowId, includePrivate) {
+ const messages = Services.console.getMessageArray();
+ return messages
+ .filter(message => {
+ if (message instanceof Ci.nsIScriptError) {
+ if (!includePrivate && message.isFromPrivateWindow) {
+ return false;
+ }
+
+ if (windowId && windowId !== message.innerWindowID) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // If this is not an nsIScriptError and we need to do window-based
+ // filtering we skip this message.
+ return false;
+ })
+ .map(error => {
+ const {
+ timeStamp,
+ errorMessage,
+ sourceName,
+ lineNumber,
+ columnNumber,
+ logLevel,
+ } = error;
+ const message = {
+ level: LOG_LEVELS[logLevel],
+ log: [errorMessage],
+ uri: sourceName,
+ pos: `${lineNumber}:${columnNumber}`,
+ };
+ return { timeStamp, message };
+ });
+ }
+
+ _getLoggedMessages(includePrivate = false) {
+ const windowId = this.contentWindow.windowGlobalChild.innerWindowId;
+ return this._getConsoleMessages(windowId).concat(
+ this._getScriptErrors(windowId, includePrivate)
+ );
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "GetLog":
+ return this._getLoggedMessages();
+ case "GetBlockingStatus":
+ const { docShell } = this;
+ return {
+ hasTrackingContentBlocked: docShell.hasTrackingContentBlocked,
+ };
+ }
+ return null;
+ }
+}
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js
new file mode 100644
index 0000000000..23d224dd98
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI */
+
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function isTelemetryEnabled() {
+ return Services.prefs.getBoolPref(
+ "datareporting.healthreport.uploadEnabled",
+ false
+ );
+}
+
+function isWebRenderEnabled() {
+ return (
+ Services.prefs.getBoolPref("gfx.webrender.all", false) ||
+ Services.prefs.getBoolPref("gfx.webrender.enabled", false)
+ );
+}
+
+this.browserInfo = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ browserInfo: {
+ async getGraphicsPrefs() {
+ const prefs = {};
+ for (const [name, dflt] of Object.entries({
+ "layers.acceleration.force-enabled": false,
+ "gfx.webrender.all": false,
+ "gfx.webrender.blob-images": true,
+ "gfx.webrender.enabled": false,
+ "image.mem.shared": true,
+ })) {
+ prefs[name] = Services.prefs.getBoolPref(name, dflt);
+ }
+ return prefs;
+ },
+ async getAppVersion() {
+ return AppConstants.MOZ_APP_VERSION;
+ },
+ async getBlockList() {
+ const trackingTable = Services.prefs.getCharPref(
+ "urlclassifier.trackingTable"
+ );
+ // If content-track-digest256 is in the tracking table,
+ // the user has enabled the strict list.
+ return trackingTable.includes("content") ? "strict" : "basic";
+ },
+ async getBuildID() {
+ return Services.appinfo.appBuildID;
+ },
+ async getUpdateChannel() {
+ return AppConstants.MOZ_UPDATE_CHANNEL;
+ },
+ async getPlatform() {
+ return AppConstants.platform;
+ },
+ async hasTouchScreen() {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfo
+ );
+ return gfxInfo.getInfo().ApzTouchInput == 1;
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json
new file mode 100644
index 0000000000..dc437c9bc9
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/browserInfo.json
@@ -0,0 +1,57 @@
+[
+ {
+ "namespace": "browserInfo",
+ "description": "experimental API extensions to get browser info not exposed via web APIs",
+ "functions": [
+ {
+ "name": "getAppVersion",
+ "type": "function",
+ "description": "Gets the app version",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getBlockList",
+ "type": "function",
+ "description": "Gets the current blocklist",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getBuildID",
+ "type": "function",
+ "description": "Gets the build ID",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getGraphicsPrefs",
+ "type": "function",
+ "description": "Gets interesting about:config prefs for graphics",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getPlatform",
+ "type": "function",
+ "description": "Gets the platform",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "getUpdateChannel",
+ "type": "function",
+ "description": "Gets the update channel",
+ "parameters": [],
+ "async": true
+ },
+ {
+ "name": "hasTouchScreen",
+ "type": "function",
+ "description": "Gets whether a touchscreen is present",
+ "parameters": [],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/l10n.js b/browser/extensions/report-site-issue/experimentalAPIs/l10n.js
new file mode 100644
index 0000000000..e87569562c
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, XPCOMUtils */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "l10nStrings", function() {
+ return Services.strings.createBundle(
+ "chrome://report-site-issue/locale/webcompat.properties"
+ );
+});
+
+let l10nManifest;
+
+this.l10n = class extends ExtensionAPI {
+ onShutdown(isAppShutdown) {
+ if (!isAppShutdown && l10nManifest) {
+ Components.manager.removeBootstrappedManifestLocation(l10nManifest);
+ }
+ }
+ getAPI(context) {
+ // Until we move to Fluent (bug 1446164), we're stuck with
+ // chrome.manifest for handling localization since its what the
+ // build system can handle for localized repacks.
+ if (context.extension.rootURI instanceof Ci.nsIJARURI) {
+ l10nManifest = context.extension.rootURI.JARFile.QueryInterface(
+ Ci.nsIFileURL
+ ).file;
+ } else if (context.extension.rootURI instanceof Ci.nsIFileURL) {
+ l10nManifest = context.extension.rootURI.file;
+ }
+
+ if (l10nManifest) {
+ Components.manager.addBootstrappedManifestLocation(l10nManifest);
+ } else {
+ Cu.reportError(
+ "Cannot find webcompat reporter chrome.manifest for registring translated strings"
+ );
+ }
+
+ return {
+ l10n: {
+ getMessage(name) {
+ try {
+ return Promise.resolve(l10nStrings.GetStringFromName(name));
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/l10n.json b/browser/extensions/report-site-issue/experimentalAPIs/l10n.json
new file mode 100644
index 0000000000..fd10cf68e7
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.json
@@ -0,0 +1,19 @@
+[
+ {
+ "namespace": "l10n",
+ "description": "A stop-gap L10N API only meant to be used until a Fluent-based API is added in bug 1425104",
+ "functions": [
+ {
+ "name": "getMessage",
+ "type": "function",
+ "description": "Gets the message with the given name",
+ "parameters": [{
+ "name": "name",
+ "type": "string",
+ "description": "The name of the message"
+ }],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js
new file mode 100644
index 0000000000..19ee738a60
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI */
+
+this.pageActionExtras = class extends ExtensionAPI {
+ getAPI(context) {
+ const extension = context.extension;
+ const pageActionAPI = extension.apiManager.getAPI(
+ "pageAction",
+ extension,
+ context.envType
+ );
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.import("resource://gre/modules/Extension.jsm", null);
+ return {
+ pageActionExtras: {
+ async setDefaultTitle(title) {
+ pageActionAPI.action.getContextData(null).title = title;
+ // Make sure the new default title is considered right away
+ for (const window of windowTracker.browserWindows()) {
+ const tab = window.gBrowser.selectedTab;
+ if (pageActionAPI.action.isShownForTab(tab)) {
+ pageActionAPI.updateButton(window);
+ }
+ }
+ },
+ async setLabelForHistogram(label) {
+ pageActionAPI.browserPageAction._labelForHistogram = label;
+ },
+ async setTooltipText(text) {
+ pageActionAPI.browserPageAction.setTooltip(text);
+ },
+ },
+ };
+ }
+};
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json
new file mode 100644
index 0000000000..df848e439a
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/pageActionExtras.json
@@ -0,0 +1,41 @@
+[
+ {
+ "namespace": "pageActionExtras",
+ "description": "experimental pageAction API extensions",
+ "functions": [
+ {
+ "name": "setDefaultTitle",
+ "type": "function",
+ "async": true,
+ "description": "Set the page action's title for all tabs",
+ "parameters": [{
+ "name": "title",
+ "type": "string",
+ "description": "title"
+ }]
+ },
+ {
+ "name": "setLabelForHistogram",
+ "type": "function",
+ "async": true,
+ "description": "Set the page action's label for telemetry histograms",
+ "parameters": [{
+ "name": "label",
+ "type": "string",
+ "description": "label for the histogram"
+ }]
+ },
+ {
+ "name": "setTooltipText",
+ "type": "function",
+ "async": true,
+ "description": "Set the page action's tooltip text",
+ "parameters": [{
+ "name": "text",
+ "type": "string",
+ "description": "text"
+ }]
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js
new file mode 100644
index 0000000000..7da789f905
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global ExtensionAPI, XPCOMUtils */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler"
+);
+
+this.tabExtras = class extends ExtensionAPI {
+ constructor(extension) {
+ super(extension);
+ this._registerActorModule();
+ }
+
+ getAPI(context) {
+ const { tabManager } = context.extension;
+ return {
+ tabExtras: {
+ async getWebcompatInfo(tabId) {
+ const { browsingContext } = tabManager.get(tabId).browser;
+ const actors = gatherActors("ReportSiteIssueHelper", browsingContext);
+ const promises = actors.map(actor => actor.sendQuery("GetLog"));
+ const logs = await Promise.all(promises);
+ const info = await actors[0].sendQuery("GetBlockingStatus");
+ info.hasMixedActiveContentBlocked = !!(
+ browsingContext.secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT
+ );
+ info.hasMixedDisplayContentBlocked = !!(
+ browsingContext.secureBrowserUI.state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT
+ );
+ info.log = logs
+ .flat()
+ .sort((a, b) => a.timeStamp - b.timeStamp)
+ .map(m => m.message);
+ return info;
+ },
+ },
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ this._unregisterActorModule();
+ }
+
+ _registerActorModule() {
+ resProto.setSubstitution(
+ "report-site-issue",
+ Services.io.newURI(
+ "experimentalAPIs/actors/",
+ null,
+ this.extension.rootURI
+ )
+ );
+ ChromeUtils.registerWindowActor("ReportSiteIssueHelper", {
+ child: {
+ moduleURI: "resource://report-site-issue/tabExtrasActor.jsm",
+ },
+ allFrames: true,
+ });
+ }
+
+ _unregisterActorModule() {
+ ChromeUtils.unregisterWindowActor("ReportSiteIssueHelper");
+ resProto.setSubstitution("report-site-issue", null);
+ }
+};
+
+function getActorForBrowsingContext(name, browsingContext) {
+ const windowGlobal = browsingContext.currentWindowGlobal;
+ return windowGlobal ? windowGlobal.getActor(name) : null;
+}
+
+function gatherActors(name, browsingContext) {
+ const list = [];
+
+ const actor = getActorForBrowsingContext(name, browsingContext);
+ if (actor) {
+ list.push(actor);
+ }
+
+ for (const child of browsingContext.children) {
+ list.push(...gatherActors(name, child));
+ }
+
+ return list;
+}
diff --git a/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json
new file mode 100644
index 0000000000..d59230b2f6
--- /dev/null
+++ b/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.json
@@ -0,0 +1,19 @@
+[
+ {
+ "namespace": "tabExtras",
+ "description": "experimental tab API extensions",
+ "functions": [
+ {
+ "name": "getWebcompatInfo",
+ "type": "function",
+ "description": "Gets the content blocking status and script log for a given tab",
+ "parameters": [{
+ "type": "integer",
+ "name": "tabId",
+ "minimum": 0
+ }],
+ "async": true
+ }
+ ]
+ }
+]
diff --git a/browser/extensions/report-site-issue/icons/lightbulb.svg b/browser/extensions/report-site-issue/icons/lightbulb.svg
new file mode 100644
index 0000000000..1f1e34da4f
--- /dev/null
+++ b/browser/extensions/report-site-issue/icons/lightbulb.svg
@@ -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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M8 0C4.3 0 2 2.107 2 5.5c0 2.372 2.065 4.268 3 5V14c0 1.476 1.616 2 3 2s3-.524 3-2v-3.5c.935-.736 3-2.632 3-5C14 2.107 11.7 0 8 0zm1 12H7v-1h2zm-1 2a3.086 3.086 0 0 1-1-.172V13h2v.828A3.047 3.047 0 0 1 8 14zm1.445-4.832A1 1 0 0 0 9 10H7a1 1 0 0 0-.444-.831C5.845 8.691 4 7.1 4 5.5 4 2.607 6.175 2 8 2s4 .607 4 3.5c0 1.6-1.845 3.191-2.555 3.668z"/>
+</svg>
diff --git a/browser/extensions/report-site-issue/locales/en-US/webcompat.properties b/browser/extensions/report-site-issue/locales/en-US/webcompat.properties
new file mode 100644
index 0000000000..ee8cab2cf0
--- /dev/null
+++ b/browser/extensions/report-site-issue/locales/en-US/webcompat.properties
@@ -0,0 +1,10 @@
+# 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/.
+
+# LOCALIZATION NOTE(wc-reporter.label2): This string will be used in the
+# Firefox page actions menu. Localized length should be considered.
+wc-reporter.label2=Report Site Issue…
+# LOCALIZATION NOTE(wc-reporter.tooltip): A site compatibility issue is
+# a website bug that exists in one browser (Firefox), but not another.
+wc-reporter.tooltip=Report a site compatibility issue
diff --git a/browser/extensions/report-site-issue/locales/jar.mn b/browser/extensions/report-site-issue/locales/jar.mn
new file mode 100644
index 0000000000..3422f6248f
--- /dev/null
+++ b/browser/extensions/report-site-issue/locales/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 report-site-issue @AB_CD@ %locale/@AB_CD@/
+ locale/@AB_CD@/webcompat.properties (%webcompat.properties)
diff --git a/browser/extensions/report-site-issue/locales/moz.build b/browser/extensions/report-site-issue/locales/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/browser/extensions/report-site-issue/locales/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"]
diff --git a/browser/extensions/report-site-issue/manifest.json b/browser/extensions/report-site-issue/manifest.json
new file mode 100644
index 0000000000..d0afa9e795
--- /dev/null
+++ b/browser/extensions/report-site-issue/manifest.json
@@ -0,0 +1,78 @@
+{
+ "manifest_version": 2,
+ "name": "WebCompat Reporter",
+ "description": "Report site compatibility issues on webcompat.com",
+ "author": "Thomas Wisniewski <twisniewski@mozilla.com>",
+ "version": "1.4.0",
+ "homepage_url": "https://github.com/mozilla/webcompat-reporter",
+ "applications": {
+ "gecko": {
+ "id": "webcompat-reporter@mozilla.org"
+ }
+ },
+ "experiment_apis": {
+ "aboutConfigPrefs": {
+ "schema": "experimentalAPIs/aboutConfigPrefs.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/aboutConfigPrefs.js",
+ "paths": [["aboutConfigPrefs"]]
+ }
+ },
+ "browserInfo": {
+ "schema": "experimentalAPIs/browserInfo.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/browserInfo.js",
+ "paths": [["browserInfo"]]
+ }
+ },
+ "l10n": {
+ "schema": "experimentalAPIs/l10n.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/l10n.js",
+ "paths": [["l10n"]]
+ }
+ },
+ "pageActionExtras": {
+ "schema": "experimentalAPIs/pageActionExtras.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/pageActionExtras.js",
+ "paths": [["pageActionExtras"]]
+ }
+ },
+ "tabExtras": {
+ "schema": "experimentalAPIs/tabExtras.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "experimentalAPIs/tabExtras.js",
+ "paths": [["tabExtras"]]
+ }
+ }
+ },
+ "icons": {
+ "16": "icons/lightbulb.svg",
+ "32": "icons/lightbulb.svg",
+ "48": "icons/lightbulb.svg",
+ "96": "icons/lightbulb.svg",
+ "128": "icons/lightbulb.svg"
+ },
+ "permissions": [
+ "tabs",
+ "<all_urls>"
+ ],
+ "background": {
+ "scripts": [
+ "background.js"
+ ]
+ },
+ "page_action": {
+ "browser_style": true,
+ "default_icon": "icons/lightbulb.svg",
+ "default_title": "Report Site Issue…",
+ "pinned": false,
+ "show_matches": ["http://*/*", "https://*/*"]
+ }
+}
diff --git a/browser/extensions/report-site-issue/moz.build b/browser/extensions/report-site-issue/moz.build
new file mode 100644
index 0000000000..e3e92a3319
--- /dev/null
+++ b/browser/extensions/report-site-issue/moz.build
@@ -0,0 +1,41 @@
+# -*- 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 += ["locales"]
+
+FINAL_TARGET_FILES.features["webcompat-reporter@mozilla.org"] += [
+ "background.js",
+ "manifest.json",
+]
+
+FINAL_TARGET_FILES.features["webcompat-reporter@mozilla.org"].experimentalAPIs += [
+ "experimentalAPIs/aboutConfigPrefs.js",
+ "experimentalAPIs/aboutConfigPrefs.json",
+ "experimentalAPIs/browserInfo.js",
+ "experimentalAPIs/browserInfo.json",
+ "experimentalAPIs/l10n.js",
+ "experimentalAPIs/l10n.json",
+ "experimentalAPIs/pageActionExtras.js",
+ "experimentalAPIs/pageActionExtras.json",
+ "experimentalAPIs/tabExtras.js",
+ "experimentalAPIs/tabExtras.json",
+]
+
+FINAL_TARGET_FILES.features[
+ "webcompat-reporter@mozilla.org"
+].experimentalAPIs.actors += ["experimentalAPIs/actors/tabExtrasActor.jsm"]
+
+FINAL_TARGET_FILES.features["webcompat-reporter@mozilla.org"].icons += [
+ "icons/lightbulb.svg"
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Web Compatibility", "Tooling & Investigations")
diff --git a/browser/extensions/report-site-issue/test/browser/.eslintrc.js b/browser/extensions/report-site-issue/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/extensions/report-site-issue/test/browser/browser.ini b/browser/extensions/report-site-issue/test/browser/browser.ini
new file mode 100644
index 0000000000..39861d8cc6
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ frameworks.html
+ fastclick.html
+ head.js
+ test.html
+ webcompat.html
+
+[browser_disabled_cleanup.js]
+[browser_button_state.js]
+[browser_report_site_issue.js]
diff --git a/browser/extensions/report-site-issue/test/browser/browser_button_state.js b/browser/extensions/report-site-issue/test/browser/browser_button_state.js
new file mode 100644
index 0000000000..d97a8460d8
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser_button_state.js
@@ -0,0 +1,93 @@
+"use strict";
+
+const REPORTABLE_PAGE = "http://example.com/";
+const REPORTABLE_PAGE2 = "https://example.com/";
+const NONREPORTABLE_PAGE = "about:mozilla";
+
+/* Test that the Report Site Issue panel item is enabled for http and https tabs,
+ on page load, or TabSelect, and disabled for everything else. */
+add_task(async function test_button_state_disabled() {
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_WC_REPORTER_ENABLED, true]] });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ REPORTABLE_PAGE
+ );
+ await openPageActions();
+ is(
+ await isPanelItemEnabled(),
+ true,
+ "Check that panel item is enabled for reportable schemes on tab load"
+ );
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ NONREPORTABLE_PAGE
+ );
+ await openPageActions();
+ is(
+ await isPanelItemDisabled(),
+ true,
+ "Check that panel item is disabled for non-reportable schemes on tab load"
+ );
+
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ REPORTABLE_PAGE2
+ );
+ await openPageActions();
+ is(
+ await isPanelItemEnabled(),
+ true,
+ "Check that panel item is enabled for reportable schemes on tab load"
+ );
+
+ await BrowserTestUtils.removeTab(tab1);
+ await BrowserTestUtils.removeTab(tab2);
+ await BrowserTestUtils.removeTab(tab3);
+});
+
+/* Test that the button is enabled or disabled when we expected it to be, when
+ pinned to the URL bar. */
+add_task(async function test_button_state_in_urlbar() {
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_WC_REPORTER_ENABLED, true]] });
+
+ pinToURLBar();
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ REPORTABLE_PAGE
+ );
+ await openPageActions();
+ is(
+ await isURLButtonPresent(),
+ true,
+ "Check that urlbar icon is enabled for reportable schemes on tab load"
+ );
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ NONREPORTABLE_PAGE
+ );
+ await openPageActions();
+ is(
+ await isURLButtonPresent(),
+ false,
+ "Check that urlbar icon is hidden for non-reportable schemes on tab load"
+ );
+
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ REPORTABLE_PAGE2
+ );
+ await openPageActions();
+ is(
+ await isURLButtonPresent(),
+ true,
+ "Check that urlbar icon is enabled for reportable schemes on tab load"
+ );
+
+ unpinFromURLBar();
+ await BrowserTestUtils.removeTab(tab1);
+ await BrowserTestUtils.removeTab(tab2);
+ await BrowserTestUtils.removeTab(tab3);
+});
diff --git a/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js b/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js
new file mode 100644
index 0000000000..fac7a5022e
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser_disabled_cleanup.js
@@ -0,0 +1,53 @@
+"use strict";
+
+// Test the addon is cleaning up after itself when disabled.
+add_task(async function test_disabled() {
+ await promiseAddonEnabled();
+
+ pinToURLBar();
+
+ SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false);
+ await promisePageActionRemoved();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http://example.com" },
+ async function() {
+ await openPageActions();
+ is(
+ await isPanelItemPresent(),
+ false,
+ "Report Site Issue button is not shown on the popup panel."
+ );
+ is(
+ await isURLButtonPresent(),
+ false,
+ "Report Site Issue is not shown on the url bar."
+ );
+ }
+ );
+
+ await promiseAddonEnabled();
+
+ pinToURLBar();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http://example.com" },
+ async function() {
+ await openPageActions();
+ is(
+ await isPanelItemEnabled(),
+ true,
+ "Report Site Issue button is shown on the popup panel."
+ );
+ is(
+ await isURLButtonPresent(),
+ true,
+ "Report Site Issue is shown on the url bar."
+ );
+ }
+ );
+
+ // Shut down the addon at the end,or the new instance started when we re-enabled it will "leak".
+ SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, false);
+ await promisePageActionRemoved();
+});
diff --git a/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js
new file mode 100644
index 0000000000..f3d34990ba
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js
@@ -0,0 +1,291 @@
+"use strict";
+
+async function clickToReportAndAwaitReportTabLoad() {
+ await openPageActions();
+ await isPanelItemEnabled();
+
+ // click on "report site issue" and wait for the new tab to open
+ const tab = await new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ event => {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ document.getElementById(WC_PAGE_ACTION_PANEL_ID).click();
+ });
+
+ // wait for the new tab to acknowledge that it received a screenshot
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "ScreenshotReceived",
+ false,
+ null,
+ true
+ );
+
+ return tab;
+}
+
+add_task(async function start_issue_server() {
+ requestLongerTimeout(2);
+
+ const serverLanding = await startIssueServer();
+
+ // ./head.js sets the value for PREF_WC_REPORTER_ENDPOINT
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["datareporting.healthreport.uploadEnabled", true],
+ [PREF_WC_REPORTER_ENABLED, true],
+ [PREF_WC_REPORTER_ENDPOINT, serverLanding],
+ ],
+ });
+});
+
+/* Test that clicking on the Report Site Issue button opens a new tab
+ and sends a postMessaged blob to it. */
+add_task(async function test_opened_page() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [{ TEST_PAGE }], async function(
+ args
+ ) {
+ async function isGreen(dataUrl) {
+ const getPixel = await new Promise(resolve => {
+ const myCanvas = content.document.createElement("canvas");
+ const ctx = myCanvas.getContext("2d");
+ const img = new content.Image();
+ img.onload = () => {
+ ctx.drawImage(img, 0, 0);
+ resolve((x, y) => {
+ return ctx.getImageData(x, y, 1, 1).data;
+ });
+ };
+ img.src = dataUrl;
+ });
+ function isPixelGreenFuzzy(p) {
+ // jpeg, so it will be off slightly
+ const fuzz = 4;
+ return p[0] < fuzz && Math.abs(p[1] - 128) < fuzz && p[2] < fuzz;
+ }
+ ok(isPixelGreenFuzzy(getPixel(0, 0)), "The pixels were green");
+ }
+
+ let doc = content.document;
+ let urlParam = doc.getElementById("url").innerText;
+ let preview = doc.getElementById("screenshot-preview");
+ const URL =
+ "http://example.com/browser/browser/extensions/report-site-issue/test/browser/test.html";
+ is(
+ urlParam,
+ args.TEST_PAGE,
+ "Reported page is correctly added to the url param"
+ );
+
+ let docShell = content.docShell;
+ is(
+ typeof docShell.getHasTrackingContentBlocked,
+ "function",
+ "docShell.hasTrackingContentBlocked is available"
+ );
+
+ let detailsParam = doc.getElementById("details").innerText;
+ const details = JSON.parse(detailsParam);
+ ok(
+ typeof details == "object",
+ "Details param is a stringified JSON object."
+ );
+ ok(Array.isArray(details.consoleLog), "Details has a consoleLog array.");
+
+ const log1 = details.consoleLog[0];
+ ok(log1.log[0] === null, "Can handle degenerate console logs");
+ ok(log1.level === "log", "Reports correct log level");
+ ok(log1.uri === URL, "Reports correct url");
+ ok(log1.pos === "7:13", "Reports correct line and column");
+
+ const log2 = details.consoleLog[1];
+ ok(log2.log[0] === "colored message", "Can handle fancy console logs");
+ ok(log2.level === "error", "Reports correct log level");
+ ok(log2.uri === URL, "Reports correct url");
+ ok(log2.pos === "8:13", "Reports correct line and column");
+
+ const log3 = details.consoleLog[2];
+ const loggedObject = log3.log[0];
+ is(loggedObject.testobj, "{...}", "Reports object inside object");
+ is(loggedObject.testnumber, 1, "Reports number inside object");
+ is(loggedObject.testArray, "(4)[...]", "Reports array inside object");
+ is(loggedObject.testUndf, "undefined", "Reports undefined inside object");
+ is(loggedObject.testNull, null, "Reports null inside object");
+ is(
+ loggedObject.testFunc,
+ undefined,
+ "Reports function inside object as undefined due to security reasons"
+ );
+ is(loggedObject.testString, "string", "Reports string inside object");
+ is(loggedObject.c, "{...}", "Reports circular reference inside object");
+ is(
+ Object.keys(loggedObject).length,
+ 10,
+ "Preview has 10 keys inside object"
+ );
+ ok(log3.level === "log", "Reports correct log level");
+ ok(log3.uri === URL, "Reports correct url");
+ ok(log3.pos === "23:13", "Reports correct line and column");
+
+ const log4 = details.consoleLog[3];
+ const loggedArray = log4.log[0];
+ is(loggedArray[0], "string", "Reports string inside array");
+ is(loggedArray[1], "{...}", "Reports object inside array");
+ is(loggedArray[2], null, "Reports null inside array");
+ is(loggedArray[3], 90, "Reports number inside array");
+ is(loggedArray[4], "undefined", "Reports undefined inside array");
+ is(
+ loggedArray[5],
+ "undefined",
+ "Reports function inside array as undefined due to security reasons"
+ );
+ is(loggedArray[6], "(4)[...]", "Reports array inside array");
+ is(loggedArray[7], "(8)[...]", "Reports circular array inside array");
+
+ const log5 = details.consoleLog[4];
+ ok(
+ log5.log[0].match(/TypeError: .*document\.access is undefined/),
+ "Script errors are logged"
+ );
+ ok(log5.level === "error", "Reports correct log level");
+ ok(log5.uri === URL, "Reports correct url");
+ ok(log5.pos === "35:5", "Reports correct line and column");
+
+ ok(typeof details.buildID == "string", "Details has a buildID string.");
+ ok(typeof details.channel == "string", "Details has a channel string.");
+ ok(
+ typeof details.hasTouchScreen == "boolean",
+ "Details has a hasTouchScreen flag."
+ );
+ ok(
+ typeof details.hasFastClick == "undefined",
+ "Details does not have FastClick if not found."
+ );
+ ok(
+ typeof details.hasMobify == "undefined",
+ "Details does not have Mobify if not found."
+ );
+ ok(
+ typeof details.hasMarfeel == "undefined",
+ "Details does not have Marfeel if not found."
+ );
+ ok(
+ typeof details["mixed active content blocked"] == "boolean",
+ "Details has a mixed active content blocked flag."
+ );
+ ok(
+ typeof details["mixed passive content blocked"] == "boolean",
+ "Details has a mixed passive content blocked flag."
+ );
+ ok(
+ typeof details["tracking content blocked"] == "string",
+ "Details has a tracking content blocked string."
+ );
+ ok(
+ typeof details["gfx.webrender.all"] == "boolean",
+ "Details has gfx.webrender.all."
+ );
+ ok(
+ typeof details["gfx.webrender.blob-images"] == "boolean",
+ "Details has gfx.webrender.blob-images."
+ );
+ ok(
+ typeof details["gfx.webrender.enabled"] == "boolean",
+ "Details has gfx.webrender.enabled."
+ );
+ ok(
+ typeof details["image.mem.shared"] == "boolean",
+ "Details has image.mem.shared."
+ );
+
+ is(
+ preview.innerText,
+ "Pass",
+ "A Blob object was successfully transferred to the test page."
+ );
+
+ const bgUrl = preview.style.backgroundImage.match(/url\(\"(.*)\"\)/)[1];
+ ok(
+ bgUrl.startsWith("data:image/jpeg;base64,"),
+ "A jpeg screenshot was successfully postMessaged"
+ );
+ await isGreen(bgUrl);
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_framework_detection() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FRAMEWORKS_TEST_PAGE
+ );
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function(args) {
+ let doc = content.document;
+ let detailsParam = doc.getElementById("details").innerText;
+ const details = JSON.parse(detailsParam);
+ ok(
+ typeof details == "object",
+ "Details param is a stringified JSON object."
+ );
+ is(details.hasFastClick, true, "FastClick was found.");
+ is(details.hasMobify, true, "Mobify was found.");
+ is(details.hasMarfeel, true, "Marfeel was found.");
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_fastclick_detection() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FASTCLICK_TEST_PAGE
+ );
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function(args) {
+ let doc = content.document;
+ let detailsParam = doc.getElementById("details").innerText;
+ const details = JSON.parse(detailsParam);
+ ok(
+ typeof details == "object",
+ "Details param is a stringified JSON object."
+ );
+ is(details.hasFastClick, true, "FastClick was found.");
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+add_task(async function test_framework_label() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ FRAMEWORKS_TEST_PAGE
+ );
+ let tab2 = await clickToReportAndAwaitReportTabLoad();
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function(args) {
+ let doc = content.document;
+ let labelParam = doc.getElementById("label").innerText;
+ const label = JSON.parse(labelParam);
+ ok(typeof label == "object", "Label param is a stringified JSON object.");
+ is(label.includes("type-fastclick"), true, "FastClick was found.");
+ is(label.includes("type-mobify"), true, "Mobify was found.");
+ is(label.includes("type-marfeel"), true, "Marfeel was found.");
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+});
diff --git a/browser/extensions/report-site-issue/test/browser/fastclick.html b/browser/extensions/report-site-issue/test/browser/fastclick.html
new file mode 100644
index 0000000000..e13329dfd7
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/fastclick.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ "use strict";
+ function ObscuredFastClick() {
+ }
+ ObscuredFastClick.prototype = {
+ needsClick: () => {},
+ };
+ window.someRandomVar = new ObscuredFastClick();
+</script>
diff --git a/browser/extensions/report-site-issue/test/browser/frameworks.html b/browser/extensions/report-site-issue/test/browser/frameworks.html
new file mode 100644
index 0000000000..14df387ec9
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/frameworks.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ "use strict";
+ function FastClick() {}
+ function marfeel() {}
+ var Mobify = {Tag: "something"};
+</script>
diff --git a/browser/extensions/report-site-issue/test/browser/head.js b/browser/extensions/report-site-issue/test/browser/head.js
new file mode 100644
index 0000000000..2779e7dfbb
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/head.js
@@ -0,0 +1,204 @@
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { Management } = ChromeUtils.import(
+ "resource://gre/modules/Extension.jsm",
+ null
+);
+
+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 FRAMEWORKS_TEST_PAGE = TEST_ROOT + "frameworks.html";
+const FASTCLICK_TEST_PAGE = TEST_ROOT + "fastclick.html";
+const NEW_ISSUE_PAGE = TEST_ROOT + "webcompat.html";
+
+const WC_ADDON_ID = "webcompat-reporter@mozilla.org";
+
+const WC_PAGE_ACTION_ID = "webcompat-reporter_mozilla_org";
+const WC_PAGE_ACTION_PANEL_ID =
+ "pageAction-panel-webcompat-reporter_mozilla_org";
+const WC_PAGE_ACTION_URLBAR_ID =
+ "pageAction-urlbar-webcompat-reporter_mozilla_org";
+
+const oldPAadd = PageActions.addAction;
+const placedSignal = "webext-page-action-placed";
+PageActions.addAction = function(action) {
+ oldPAadd.call(this, action);
+ if (action.id === WC_PAGE_ACTION_ID) {
+ Services.obs.notifyObservers(null, placedSignal);
+ }
+ return action;
+};
+const oldPAremoved = PageActions.onActionRemoved;
+const removedSignal = "webext-page-action-removed";
+PageActions.onActionRemoved = function(action) {
+ oldPAremoved.call(this, action);
+ if (action.id === WC_PAGE_ACTION_ID) {
+ Services.obs.notifyObservers(null, removedSignal);
+ }
+ return action;
+};
+
+function promisePageActionSignal(signal) {
+ return new Promise(done => {
+ const obs = function() {
+ Services.obs.removeObserver(obs, signal);
+ done();
+ };
+ Services.obs.addObserver(obs, signal);
+ });
+}
+
+function promisePageActionPlaced() {
+ return promisePageActionSignal(placedSignal);
+}
+
+function promisePageActionRemoved() {
+ return promisePageActionSignal(removedSignal);
+}
+
+async function promiseAddonEnabled() {
+ const addon = await AddonManager.getAddonByID(WC_ADDON_ID);
+ if (addon.isActive) {
+ return;
+ }
+ const pref = SpecialPowers.Services.prefs.getBoolPref(
+ PREF_WC_REPORTER_ENABLED,
+ false
+ );
+ if (!pref) {
+ SpecialPowers.Services.prefs.setBoolPref(PREF_WC_REPORTER_ENABLED, true);
+ }
+ await promisePageActionPlaced();
+}
+
+async function isPanelItemEnabled() {
+ const icon = document.getElementById(WC_PAGE_ACTION_PANEL_ID);
+ return icon && !icon.disabled;
+}
+
+async function isPanelItemDisabled() {
+ const icon = document.getElementById(WC_PAGE_ACTION_PANEL_ID);
+ return icon && icon.disabled;
+}
+
+async function isPanelItemPresent() {
+ return document.getElementById(WC_PAGE_ACTION_PANEL_ID) !== null;
+}
+
+async function isURLButtonPresent() {
+ return document.getElementById(WC_PAGE_ACTION_URLBAR_ID) !== null;
+}
+
+function openPageActions() {
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ // Wait for the main page action button to become visible. It's hidden for
+ // some URIs, so depending on when this is called, it may not yet be quite
+ // visible. It's up to the caller to make sure it will be visible.
+ info("Waiting for main page action button to have non-0 size");
+ let bounds = dwu.getBoundsWithoutFlushing(
+ BrowserPageActions.mainButtonNode
+ );
+ return bounds.width > 0 && bounds.height > 0;
+ })
+ .then(() => {
+ // Wait for the panel to become open, by clicking the button if necessary.
+ info("Waiting for main page action panel to be open");
+ if (BrowserPageActions.panelNode.state == "open") {
+ return Promise.resolve();
+ }
+ let shownPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ return shownPromise;
+ })
+ .then(() => {
+ // Wait for items in the panel to become visible.
+ return promisePageActionViewChildrenVisible(
+ BrowserPageActions.mainViewNode
+ );
+ });
+}
+
+function promisePageActionPanelShown() {
+ return new Promise(resolve => {
+ if (BrowserPageActions.panelNode.state == "open") {
+ executeSoon(resolve);
+ return;
+ }
+ BrowserPageActions.panelNode.addEventListener(
+ "popupshown",
+ () => {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+ info(
+ "promisePageActionViewChildrenVisible waiting for a child node to be visible"
+ );
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ let bodyNode = panelViewNode.firstElementChild;
+ for (let childNode of bodyNode.children) {
+ let bounds = dwu.getBoundsWithoutFlushing(childNode);
+ if (bounds.width > 0 && bounds.height > 0) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
+
+function pinToURLBar() {
+ PageActions.actionForID(WC_PAGE_ACTION_ID).pinnedToUrlbar = true;
+}
+
+function unpinFromURLBar() {
+ PageActions.actionForID(WC_PAGE_ACTION_ID).pinnedToUrlbar = false;
+}
+
+async function startIssueServer() {
+ const landingTemplate = await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", NEW_ISSUE_PAGE);
+ xhr.onload = () => {
+ resolve(xhr.responseText);
+ };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+
+ const { HttpServer } = ChromeUtils.import(
+ "resource://testing-common/httpd.js"
+ );
+ const server = new HttpServer();
+
+ registerCleanupFunction(async function cleanup() {
+ await new Promise(resolve => server.stop(resolve));
+ });
+
+ server.registerPathHandler("/new", function(request, response) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(landingTemplate);
+ });
+
+ server.start(-1);
+ return `http://localhost:${server.identity.primaryPort}/new`;
+}
diff --git a/browser/extensions/report-site-issue/test/browser/test.html b/browser/extensions/report-site-issue/test/browser/test.html
new file mode 100644
index 0000000000..e2bae1d80a
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ /* eslint-disable no-console */
+ /* eslint-disable no-unused-expressions */
+ "use strict";
+ console.log(null);
+ console.error("%ccolored message", "background:green; color:white");
+ const obj = {
+ testobj: {},
+ testnumber: 1,
+ testArray: [1, {}, 2, 555],
+ testUndf: undefined,
+ testNull: null,
+ testFunc() {},
+ testString: 'string',
+ prop1: 'prop1',
+ prop2: 'prop2'
+ };
+ obj.c = obj;
+ obj.prop3 = 'prop3';
+ obj.prop4 = 'prop4';
+ console.log(obj);
+ const arr = [
+ 'string',
+ {test: 'obj'},
+ null,
+ 90,
+ undefined,
+ function() {},
+ [1, {}, 2, 555]
+ ];
+ arr.push(arr);
+ console.log(arr);
+ document.access.non.existent.property.to.trigger.error;
+</script>
+<style>
+ body {background: rgb(0, 128, 0);}
+</style>
diff --git a/browser/extensions/report-site-issue/test/browser/webcompat.html b/browser/extensions/report-site-issue/test/browser/webcompat.html
new file mode 100644
index 0000000000..872c8917b7
--- /dev/null
+++ b/browser/extensions/report-site-issue/test/browser/webcompat.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+ #screenshot-preview {width: 200px; height: 200px;}
+</style>
+<div id="url">$$URL$$</div>
+<div id="details">$$DETAILS$$</div>
+<div id="label">$$LABEL$$</div>
+<div id="screenshot-preview">Fail</div>
+<script>
+"use strict";
+let preview = document.getElementById("screenshot-preview");
+const CONFIG = {
+ url: {
+ element: document.getElementById("url")
+ },
+ details: {
+ element: document.getElementById("details"),
+ toStringify: true
+ },
+ extra_labels: {
+ element: document.getElementById("label"),
+ toStringify: true
+ },
+};
+
+function getBlobAsDataURL(blob) {
+ return new Promise((resolve, reject) => {
+ let reader = new FileReader();
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ reader.addEventListener("error", (e) => {
+ reject(`There was an error reading the blob: ${e.type}`);
+ });
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ reader.addEventListener("load", (e) => {
+ resolve(e.target.result);
+ });
+
+ reader.readAsDataURL(blob);
+ });
+}
+
+function setPreviewBG(backgroundData) {
+ return new Promise((resolve) => {
+ preview.style.background = `url(${backgroundData})`;
+ resolve();
+ });
+}
+
+function sendReceivedEvent() {
+ window.dispatchEvent(new CustomEvent("ScreenshotReceived", {bubbles: true}));
+}
+
+function prepareContent(toStringify, content) {
+ if (toStringify) {
+ return JSON.stringify(content)
+ }
+
+ return content;
+}
+
+function appendMessage(message) {
+ for (const key in CONFIG) {
+ if (key in message) {
+ const field = CONFIG[key];
+ field.element.innerText = prepareContent(field.toStringify, message[key]);
+ }
+ }
+}
+
+// eslint-disable-next-line mozilla/balanced-listeners
+window.addEventListener("message", function(event) {
+ if (event.data.screenshot instanceof Blob) {
+ preview.innerText = "Pass";
+ }
+
+ if (event.data.message) {
+ appendMessage(event.data.message);
+ }
+
+ getBlobAsDataURL(event.data.screenshot).then(setPreviewBG).then(sendReceivedEvent);
+});
+</script>