242 lines
7.8 KiB
JavaScript
242 lines
7.8 KiB
JavaScript
/* 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/. */
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
// Maximum length of the string properties sent to the API endpoint.
|
|
const MAX_STRING_LENGTH = 255;
|
|
|
|
const AMO_SUPPORTED_ADDON_TYPES = [
|
|
"extension",
|
|
"theme",
|
|
"sitepermission",
|
|
"dictionary",
|
|
];
|
|
|
|
const PREF_ADDON_ABUSE_REPORT_URL = "extensions.addonAbuseReport.url";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
ClientID: "resource://gre/modules/ClientID.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"ADDON_ABUSE_REPORT_URL",
|
|
PREF_ADDON_ABUSE_REPORT_URL
|
|
);
|
|
|
|
const ERROR_TYPES = Object.freeze([
|
|
"ERROR_CLIENT",
|
|
"ERROR_NETWORK",
|
|
"ERROR_SERVER",
|
|
"ERROR_UNKNOWN",
|
|
]);
|
|
|
|
export class AbuseReportError extends Error {
|
|
constructor(errorType, errorInfo = undefined) {
|
|
if (!ERROR_TYPES.includes(errorType)) {
|
|
throw new Error(`Unexpected 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 used to manage abuse reports for add-ons.
|
|
*/
|
|
export const AbuseReporter = {
|
|
getAMOFormURL({ addonId }) {
|
|
return Services.urlFormatter
|
|
.formatURLPref("extensions.abuseReport.amoFormURL")
|
|
.replace(/%addonID%/g, addonId);
|
|
},
|
|
|
|
isSupportedAddonType(addonType) {
|
|
return AMO_SUPPORTED_ADDON_TYPES.includes(addonType);
|
|
},
|
|
|
|
/**
|
|
* Send an add-on abuse report using the AMO API. The data passed to this
|
|
* method might be augmented with report data known by Firefox.
|
|
*
|
|
* @param {string} addonId
|
|
* @param {{[key: string]: string|null}} data
|
|
* Abuse report data to be submitting to the AMO API along with the
|
|
* additional abuse report data known by Firefox.
|
|
* @param {object} [options]
|
|
* @param {string} [options.authorization]
|
|
* An optional value of an Authorization HTTP header to be set on the
|
|
* submission request.
|
|
*
|
|
* @returns {Promise<object>} Return a promise that resolves to the JSON AMO
|
|
* API response (or an error when something went wrong).
|
|
*/
|
|
async sendAbuseReport(addonId, data, options = {}) {
|
|
const rejectReportError = async (errorType, { response } = {}) => {
|
|
// 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);
|
|
};
|
|
|
|
let abuseReport = { addon: addonId, ...data };
|
|
|
|
// If the add-on is installed, augment the data with internal report data.
|
|
const addon = await lazy.AddonManager.getAddonByID(addonId);
|
|
if (addon) {
|
|
const metadata = await AbuseReporter.getReportData(addon);
|
|
abuseReport = { ...abuseReport, ...metadata };
|
|
}
|
|
|
|
const headers = { "Content-Type": "application/json" };
|
|
if (options?.authorization?.length) {
|
|
headers.authorization = options.authorization;
|
|
}
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(lazy.ADDON_ABUSE_REPORT_URL, {
|
|
method: "POST",
|
|
credentials: "omit",
|
|
referrerPolicy: "no-referrer",
|
|
headers,
|
|
body: JSON.stringify(abuseReport),
|
|
});
|
|
} catch (err) {
|
|
Cu.reportError(err);
|
|
return rejectReportError("ERROR_NETWORK");
|
|
}
|
|
|
|
if (response.ok && response.status >= 200 && response.status < 400) {
|
|
return response.json();
|
|
}
|
|
|
|
if (response.status >= 400 && response.status < 500) {
|
|
return rejectReportError("ERROR_CLIENT", { response });
|
|
}
|
|
|
|
if (response.status >= 500 && response.status < 600) {
|
|
return rejectReportError("ERROR_SERVER", { response });
|
|
}
|
|
|
|
return rejectReportError("ERROR_UNKNOWN", { response });
|
|
},
|
|
|
|
/**
|
|
* Helper function that retrieves from an addon object all the data to send
|
|
* as part of the submission request, besides the `reason`, `message` which are
|
|
* going to be received from the submit method of the report object returned
|
|
* by `createAbuseReport`.
|
|
* (See https://addons-server.readthedocs.io/en/latest/topics/api/abuse.html)
|
|
*
|
|
* @param {AddonWrapper} addon
|
|
* The addon object to collect the detail from.
|
|
*
|
|
* @return {object}
|
|
* An object that contains the collected details.
|
|
*/
|
|
async getReportData(addon) {
|
|
const truncateString = text =>
|
|
typeof text == "string" ? text.slice(0, MAX_STRING_LENGTH) : text;
|
|
|
|
// Normalize addon_install_source and addon_install_method values
|
|
// as expected by the server API endpoint. Returns null if the
|
|
// value is not a string.
|
|
const normalizeValue = text =>
|
|
typeof text == "string"
|
|
? text.toLowerCase().replace(/[- :]/g, "_")
|
|
: null;
|
|
|
|
const installInfo = addon.installTelemetryInfo || {};
|
|
|
|
const data = {
|
|
addon: addon.id,
|
|
addon_version: addon.version,
|
|
addon_name: truncateString(addon.name),
|
|
addon_summary: truncateString(addon.description),
|
|
addon_install_origin:
|
|
addon.sourceURI && truncateString(addon.sourceURI.spec),
|
|
install_date: addon.installDate && addon.installDate.toISOString(),
|
|
addon_install_source: normalizeValue(installInfo.source),
|
|
addon_install_source_url:
|
|
installInfo.sourceURL && truncateString(installInfo.sourceURL),
|
|
addon_install_method: normalizeValue(installInfo.method),
|
|
};
|
|
|
|
switch (addon.signedState) {
|
|
case lazy.AddonManager.SIGNEDSTATE_BROKEN:
|
|
data.addon_signature = "broken";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_UNKNOWN:
|
|
data.addon_signature = "unknown";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_MISSING:
|
|
data.addon_signature = "missing";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_PRELIMINARY:
|
|
data.addon_signature = "preliminary";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_SIGNED:
|
|
data.addon_signature = "signed";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_SYSTEM:
|
|
data.addon_signature = "system";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_PRIVILEGED:
|
|
data.addon_signature = "privileged";
|
|
break;
|
|
case lazy.AddonManager.SIGNEDSTATE_NOT_REQUIRED:
|
|
data.addon_signature = "not_required";
|
|
break;
|
|
default:
|
|
data.addon_signature = `unknown: ${addon.signedState}`;
|
|
}
|
|
|
|
// Set "curated" as addon_signature on recommended addons
|
|
// (addon.isRecommended internally checks that the addon is also
|
|
// signed correctly).
|
|
if (addon.isRecommended) {
|
|
data.addon_signature = "curated";
|
|
}
|
|
|
|
data.client_id = await lazy.ClientID.getClientIdHash();
|
|
|
|
data.app = AppConstants.platform === "android" ? "android" : "firefox";
|
|
data.appversion = Services.appinfo.version;
|
|
data.lang = Services.locale.appLocaleAsBCP47;
|
|
data.operating_system = AppConstants.platform;
|
|
data.operating_system_version = Services.sysinfo.getProperty("version");
|
|
|
|
return data;
|
|
},
|
|
};
|