summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/AbuseReporter.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/AbuseReporter.sys.mjs')
-rw-r--r--toolkit/mozapps/extensions/AbuseReporter.sys.mjs587
1 files changed, 4 insertions, 583 deletions
diff --git a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs
index 944cef507c..966e2a6dd5 100644
--- a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs
+++ b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs
@@ -2,310 +2,39 @@
* 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/. */
-import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
-const PREF_ABUSE_REPORT_URL = "extensions.abuseReport.url";
-const PREF_AMO_DETAILS_API_URL = "extensions.abuseReport.amoDetailsURL";
-
-// Name associated with the report dialog window.
-const DIALOG_WINDOW_NAME = "addons-abuse-report-dialog";
-
// Maximum length of the string properties sent to the API endpoint.
const MAX_STRING_LENGTH = 255;
-// Minimum time between report submissions (in ms).
-const MIN_MS_BETWEEN_SUBMITS = 30000;
-
-// The addon types currently supported by the integrated abuse report panel.
-const SUPPORTED_ADDON_TYPES = [
+const AMO_SUPPORTED_ADDON_TYPES = [
"extension",
"theme",
"sitepermission",
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
"sitepermission-deprecated",
+ "dictionary",
];
-// An expanded set of addon types supported when the abuse report hosted on AMO is enabled
-// (based on the "extensions.abuseReport.amoFormEnabled" pref).
-const AMO_SUPPORTED_ADDON_TYPES = [...SUPPORTED_ADDON_TYPES, "dictionary"];
-
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- AMTelemetry: "resource://gre/modules/AddonManager.sys.mjs",
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
ClientID: "resource://gre/modules/ClientID.sys.mjs",
});
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "ABUSE_REPORT_URL",
- PREF_ABUSE_REPORT_URL
-);
-
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "AMO_DETAILS_API_URL",
- PREF_AMO_DETAILS_API_URL
-);
-
-// Whether the abuse report feature should open a form hosted by the url
-// derived from the one set on the extensions.abuseReport.amoFormURL pref
-// or use the abuse report panel integrated in Firefox.
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "ABUSE_REPORT_AMO_FORM_ENABLED",
- "extensions.abuseReport.amoFormEnabled",
- true
-);
-
-const PRIVATE_REPORT_PROPS = Symbol("privateReportProps");
-
-const ERROR_TYPES = Object.freeze([
- "ERROR_ABORTED_SUBMIT",
- "ERROR_ADDON_NOTFOUND",
- "ERROR_CLIENT",
- "ERROR_NETWORK",
- "ERROR_UNKNOWN",
- "ERROR_RECENT_SUBMIT",
- "ERROR_SERVER",
- "ERROR_AMODETAILS_NOTFOUND",
- "ERROR_AMODETAILS_FAILURE",
-]);
-
-export class AbuseReportError extends Error {
- constructor(errorType, errorInfo = undefined) {
- if (!ERROR_TYPES.includes(errorType)) {
- throw new Error(`Unknown AbuseReportError type "${errorType}"`);
- }
-
- let message = errorInfo ? `${errorType} - ${errorInfo}` : errorType;
-
- super(message);
- this.name = "AbuseReportError";
- this.errorType = errorType;
- this.errorInfo = errorInfo;
- }
-}
-
-/**
- * Create an error info string from a fetch response object.
- *
- * @param {Response} response
- * A fetch response object to convert into an errorInfo string.
- *
- * @returns {Promise<string>}
- * The errorInfo string to be included in an AbuseReportError.
- */
-async function responseToErrorInfo(response) {
- return JSON.stringify({
- status: response.status,
- responseText: await response.text().catch(() => ""),
- });
-}
-
/**
- * A singleton object used to create new AbuseReport instances for a given addonId
- * and enforce a minium amount of time between two report submissions .
+ * A singleton used to manage abuse reports for add-ons.
*/
export const AbuseReporter = {
- _lastReportTimestamp: null,
-
- get amoFormEnabled() {
- return lazy.ABUSE_REPORT_AMO_FORM_ENABLED;
- },
-
getAMOFormURL({ addonId }) {
return Services.urlFormatter
.formatURLPref("extensions.abuseReport.amoFormURL")
.replace(/%addonID%/g, addonId);
},
- // Error types.
- updateLastReportTimestamp() {
- this._lastReportTimestamp = Date.now();
- },
-
- getTimeFromLastReport() {
- const currentTimestamp = Date.now();
- if (this._lastReportTimestamp > currentTimestamp) {
- // Reset the last report timestamp if it is in the future.
- this._lastReportTimestamp = null;
- }
-
- if (!this._lastReportTimestamp) {
- return Infinity;
- }
-
- return currentTimestamp - this._lastReportTimestamp;
- },
-
isSupportedAddonType(addonType) {
- if (this.amoFormEnabled) {
- return AMO_SUPPORTED_ADDON_TYPES.includes(addonType);
- }
- return SUPPORTED_ADDON_TYPES.includes(addonType);
- },
-
- /**
- * Create an AbuseReport instance, given the addonId and a reportEntryPoint.
- *
- * @param {string} addonId
- * The id of the addon to create the report instance for.
- * @param {object} options
- * @param {string} options.reportEntryPoint
- * An identifier that represent the entry point for the report flow.
- *
- * @returns {Promise<AbuseReport>}
- * Returns a promise that resolves to an instance of the AbuseReport
- * class, which represent an ongoing report.
- */
- async createAbuseReport(addonId, { reportEntryPoint } = {}) {
- let addon = await lazy.AddonManager.getAddonByID(addonId);
-
- if (!addon) {
- // The addon isn't installed, query the details from the AMO API endpoint.
- addon = await this.queryAMOAddonDetails(addonId, reportEntryPoint);
- }
-
- if (!addon) {
- lazy.AMTelemetry.recordReportEvent({
- addonId,
- errorType: "ERROR_ADDON_NOTFOUND",
- reportEntryPoint,
- });
- throw new AbuseReportError("ERROR_ADDON_NOTFOUND");
- }
-
- const reportData = await this.getReportData(addon);
-
- return new AbuseReport({
- addon,
- reportData,
- reportEntryPoint,
- });
- },
-
- /**
- * Retrieves the addon details from the AMO API endpoint, used to create
- * abuse reports on non-installed addon-ons.
- *
- * For the addon details that may be translated (e.g. addon name, description etc.)
- * the function will try to retrieve the string localized in the same locale used
- * by Gecko (and fallback to "en-US" if that locale is unavailable).
- *
- * The addon creator properties are set to the first author available.
- *
- * @param {string} addonId
- * The id of the addon to retrieve the details available on AMO.
- * @param {string} reportEntryPoint
- * The entry point for the report flow (to be included in the telemetry
- * recorded in case of failures).
- *
- * @returns {Promise<AMOAddonDetails|null>}
- * Returns a promise that resolves to an AMOAddonDetails object,
- * which has the subset of the AddonWrapper properties which are
- * needed by the abuse report panel or the report data sent to
- * the abuse report API endpoint), or null if it fails to
- * retrieve the details from AMO.
- *
- * @typedef {object} AMOAddonDetails
- * @prop {string} id
- * @prop {string} name
- * @prop {string} version
- * @prop {string} description
- * @prop {string} type
- * @prop {string} iconURL
- * @prop {string} homepageURL
- * @prop {string} supportURL
- * @prop {AMOAddonCreator} creator
- * @prop {boolean} isRecommended
- * @prop {number} signedState=AddonManager.SIGNEDSTATE_UNKNOWN
- * @prop {object} installTelemetryInfo={ source: "not_installed" }
- *
- * @typedef {object} AMOAddonCreator
- * @prop {string} name
- * @prop {string} url
- */
- async queryAMOAddonDetails(addonId, reportEntryPoint) {
- let details;
- try {
- // This should be the API endpoint documented at:
- // https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#detail
- details = await fetch(`${lazy.AMO_DETAILS_API_URL}/${addonId}`, {
- credentials: "omit",
- referrerPolicy: "no-referrer",
- headers: { "Content-Type": "application/json" },
- }).then(async response => {
- if (response.status === 200) {
- return response.json();
- }
-
- let errorInfo = await responseToErrorInfo(response).catch(
- () => undefined
- );
-
- if (response.status === 404) {
- // Record a different telemetry event for 404 errors.
- throw new AbuseReportError("ERROR_AMODETAILS_NOTFOUND", errorInfo);
- }
-
- throw new AbuseReportError("ERROR_AMODETAILS_FAILURE", errorInfo);
- });
- } catch (err) {
- // Log the original error in the browser console.
- Cu.reportError(err);
-
- lazy.AMTelemetry.recordReportEvent({
- addonId,
- errorType: err.errorType || "ERROR_AMODETAILS_FAILURE",
- reportEntryPoint,
- });
-
- return null;
- }
-
- const locale = Services.locale.appLocaleAsBCP47;
-
- // Get a string value from a translated value
- // (https://addons-server.readthedocs.io/en/latest/topics/api/overview.html#api-overview-translations)
- const getTranslatedValue = value => {
- if (typeof value === "string") {
- return value;
- }
- return value && (value[locale] || value["en-US"]);
- };
-
- const getAuthorField = fieldName =>
- details.authors && details.authors[0] && details.authors[0][fieldName];
-
- // Normalize type "statictheme" (which is the type used on the AMO API side)
- // into "theme" (because it is the type we use and expect on the Firefox side
- // for this addon type).
- const addonType = details.type === "statictheme" ? "theme" : details.type;
-
- return {
- id: addonId,
- name: getTranslatedValue(details.name),
- version: details.current_version.version,
- description: getTranslatedValue(details.summary),
- type: addonType,
- iconURL: details.icon_url,
- homepageURL: getTranslatedValue(details.homepage),
- supportURL: getTranslatedValue(details.support_url),
- // Set the addon creator to the first author in the AMO details.
- creator: {
- name: getAuthorField("name"),
- url: getAuthorField("url"),
- },
- isRecommended: details.is_recommended,
- // Set signed state to unknown because it isn't installed.
- signedState: lazy.AddonManager.SIGNEDSTATE_UNKNOWN,
- // Set the installTelemetryInfo.source to "not_installed".
- installTelemetryInfo: { source: "not_installed" },
- };
+ return AMO_SUPPORTED_ADDON_TYPES.includes(addonType);
},
/**
@@ -392,312 +121,4 @@ export const AbuseReporter = {
return data;
},
-
- /**
- * Helper function that returns a reference to a report dialog window
- * already opened (if any).
- *
- * @returns {Window?}
- */
- getOpenDialog() {
- return Services.ww.getWindowByName(DIALOG_WINDOW_NAME);
- },
-
- /**
- * Helper function that opens an abuse report form in a new dialog window.
- *
- * @param {string} addonId
- * The addonId being reported.
- * @param {string} reportEntryPoint
- * The entry point from which the user has triggered the abuse report
- * flow.
- * @param {XULElement} browser
- * The browser element (if any) that is opening the report window.
- *
- * @return {Promise<AbuseReportDialog>}
- * Returns an AbuseReportDialog object, rejects if it fails to open
- * the dialog.
- *
- * @typedef {object} AbuseReportDialog
- * An object that represents the abuse report dialog.
- * @prop {function} close
- * A method that closes the report dialog (used by the caller
- * to close the dialog when the user chooses to close the window
- * that started the abuse report flow).
- * @prop {Promise<AbuseReport|undefined>} promiseReport
- * A promise resolved to an AbuseReport instance if the report should
- * be submitted, or undefined if the user has cancelled the report.
- * Rejects if it fails to create an AbuseReport instance or to open
- * the abuse report window.
- */
- async openDialog(addonId, reportEntryPoint, browser) {
- const chromeWin = browser && browser.ownerGlobal;
- if (!chromeWin) {
- throw new Error("Abuse Reporter dialog cancelled, opener tab closed");
- }
-
- const dialogWin = this.getOpenDialog();
-
- if (dialogWin) {
- // If an abuse report dialog is already open, cancel the
- // previous report flow and start a new one.
- const { deferredReport, promiseReport } =
- dialogWin.arguments[0].wrappedJSObject;
- deferredReport.resolve({ userCancelled: true });
- await promiseReport;
- }
-
- const report = await AbuseReporter.createAbuseReport(addonId, {
- reportEntryPoint,
- });
-
- if (!SUPPORTED_ADDON_TYPES.includes(report.addon.type)) {
- throw new Error(
- `Addon type "${report.addon.type}" is not currently supported by the integrated abuse reporting feature`
- );
- }
-
- const params = Cc["@mozilla.org/array;1"].createInstance(
- Ci.nsIMutableArray
- );
-
- const dialogInit = {
- report,
- openWebLink(url) {
- chromeWin.openWebLinkIn(url, "tab", {
- relatedToCurrent: true,
- });
- },
- };
-
- params.appendElement(dialogInit);
-
- let win;
- function closeDialog() {
- if (win && !win.closed) {
- win.close();
- }
- }
-
- const promiseReport = new Promise((resolve, reject) => {
- dialogInit.deferredReport = { resolve, reject };
- }).then(
- ({ userCancelled }) => {
- closeDialog();
- return userCancelled ? undefined : report;
- },
- err => {
- Cu.reportError(
- `Unexpected abuse report panel error: ${err} :: ${err.stack}`
- );
- closeDialog();
- return Promise.reject({
- message: "Unexpected abuse report panel error",
- });
- }
- );
-
- const promiseReportPanel = new Promise((resolve, reject) => {
- dialogInit.deferredReportPanel = { resolve, reject };
- });
-
- dialogInit.promiseReport = promiseReport;
- dialogInit.promiseReportPanel = promiseReportPanel;
-
- win = Services.ww.openWindow(
- chromeWin,
- "chrome://mozapps/content/extensions/abuse-report-frame.html",
- DIALOG_WINDOW_NAME,
- // Set the dialog window options (including a reasonable initial
- // window height size, eventually adjusted by the panel once it
- // has been rendered its content).
- "dialog,centerscreen,height=700",
- params
- );
-
- return {
- close: closeDialog,
- promiseReport,
-
- // Properties used in tests
- promiseReportPanel,
- window: win,
- };
- },
};
-
-/**
- * Represents an ongoing abuse report. Instances of this class are created
- * by the `AbuseReporter.createAbuseReport` method.
- *
- * This object is used by the reporting UI panel and message bars to:
- *
- * - get an errorType in case of a report creation error (e.g. because of a
- * previously submitted report)
- * - get the addon details used inside the reporting panel
- * - submit the abuse report (and re-submit if a previous submission failed
- * and the user choose to retry to submit it again)
- * - abort an ongoing submission
- *
- * @param {object} options
- * @param {AddonWrapper|null} options.addon
- * AddonWrapper instance for the extension/theme being reported.
- * (May be null if the extension has not been found).
- * @param {object|null} options.reportData
- * An object which contains addon and environment details to send as part of a submission
- * (may be null if the report has a createErrorType).
- * @param {string} options.reportEntryPoint
- * A string that identify how the report has been triggered.
- */
-class AbuseReport {
- constructor({ addon, reportData, reportEntryPoint }) {
- this[PRIVATE_REPORT_PROPS] = {
- aborted: false,
- abortController: new AbortController(),
- addon,
- reportData,
- reportEntryPoint,
- // message and reason are initially null, and then set by the panel
- // using the related set method.
- message: null,
- reason: null,
- };
- }
-
- recordTelemetry(errorType) {
- const { addon, reportEntryPoint } = this;
- lazy.AMTelemetry.recordReportEvent({
- addonId: addon.id,
- addonType: addon.type,
- errorType,
- reportEntryPoint,
- });
- }
-
- /**
- * Submit the current report, given a reason and a message.
- *
- * @returns {Promise<void>}
- * Resolves once the report has been successfully submitted.
- * It rejects with an AbuseReportError if the report couldn't be
- * submitted for a known reason (or another Error type otherwise).
- */
- async submit() {
- const {
- aborted,
- abortController,
- message,
- reason,
- reportData,
- reportEntryPoint,
- } = this[PRIVATE_REPORT_PROPS];
-
- // Record telemetry event and throw an AbuseReportError.
- const rejectReportError = async (errorType, { response } = {}) => {
- this.recordTelemetry(errorType);
-
- // Leave errorInfo empty if there is no response or fails to
- // be converted into an error info object.
- const errorInfo = response
- ? await responseToErrorInfo(response).catch(() => undefined)
- : undefined;
-
- throw new AbuseReportError(errorType, errorInfo);
- };
-
- if (aborted) {
- // Report aborted before being actually submitted.
- return rejectReportError("ERROR_ABORTED_SUBMIT");
- }
-
- // Prevent submit of a new abuse report in less than MIN_MS_BETWEEN_SUBMITS.
- let msFromLastReport = AbuseReporter.getTimeFromLastReport();
- if (msFromLastReport < MIN_MS_BETWEEN_SUBMITS) {
- return rejectReportError("ERROR_RECENT_SUBMIT");
- }
-
- let response;
- try {
- response = await fetch(lazy.ABUSE_REPORT_URL, {
- signal: abortController.signal,
- method: "POST",
- credentials: "omit",
- referrerPolicy: "no-referrer",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- ...reportData,
- report_entry_point: reportEntryPoint,
- message,
- reason,
- }),
- });
- } catch (err) {
- if (err.name === "AbortError") {
- return rejectReportError("ERROR_ABORTED_SUBMIT");
- }
- Cu.reportError(err);
- return rejectReportError("ERROR_NETWORK");
- }
-
- if (response.ok && response.status >= 200 && response.status < 400) {
- // Ensure that the response is also a valid json format.
- try {
- await response.json();
- } catch (err) {
- this.recordTelemetry("ERROR_UNKNOWN");
- throw err;
- }
- AbuseReporter.updateLastReportTimestamp();
- this.recordTelemetry();
- return undefined;
- }
-
- if (response.status >= 400 && response.status < 500) {
- return rejectReportError("ERROR_CLIENT", { response });
- }
-
- if (response.status >= 500 && response.status < 600) {
- return rejectReportError("ERROR_SERVER", { response });
- }
-
- // We got an unexpected HTTP status code.
- return rejectReportError("ERROR_UNKNOWN", { response });
- }
-
- /**
- * Abort the report submission.
- */
- abort() {
- const { abortController } = this[PRIVATE_REPORT_PROPS];
- abortController.abort();
- this[PRIVATE_REPORT_PROPS].aborted = true;
- }
-
- get addon() {
- return this[PRIVATE_REPORT_PROPS].addon;
- }
-
- get reportEntryPoint() {
- return this[PRIVATE_REPORT_PROPS].reportEntryPoint;
- }
-
- /**
- * Set the open message (called from the panel when the user submit the report)
- *
- * @parm {string} message
- * An optional string which contains a description for the reported issue.
- */
- setMessage(message) {
- this[PRIVATE_REPORT_PROPS].message = message;
- }
-
- /**
- * Set the report reason (called from the panel when the user submit the report)
- *
- * @parm {string} reason
- * String identifier for the report reason.
- */
- setReason(reason) {
- this[PRIVATE_REPORT_PROPS].reason = reason;
- }
-}