Bug 1893116 - Remove built-in abuse report UI. r=rpl,geckoview-reviewers,desktop-theme-reviewers,emilio,amejiamarmol

This patch removes both the backend and UI code related to the built-in
abuse reporting feature. I kept the ability to disable abuse reporting
with the `extensions.abuseReport.enabled` pref. Other prefs related to
the old abuse reporting feature have been removed.

Some tests have been deleted because they aren't relevant anymore.

On the web API side, both the property and method have been removed
since only AMO consumes these APIs and AMO already supports the absence
of these APIs. That is also why GeckoView has been slightly updated.

We do not collect Telemetry with the new AMO (abuse report) form so
all the Telemetry bits have been removed as well.

Differential Revision: https://phabricator.services.mozilla.com/D208457
This commit is contained in:
William Durand 2024-04-29 13:56:26 +00:00
Родитель 8ab480d009
Коммит dcf4447762
28 изменённых файлов: 65 добавлений и 5761 удалений

Просмотреть файл

@ -23,8 +23,6 @@ pref("browser.hiddenWindowChromeURL", "chrome://browser/content/hiddenWindowMac.
// Set add-ons abuse report related prefs specific to Firefox Desktop.
pref("extensions.abuseReport.enabled", true);
pref("extensions.abuseReport.amWebAPI.enabled", true);
pref("extensions.abuseReport.amoFormEnabled", true);
// Enables some extra Extension System Logging (can reduce performance)
pref("extensions.logging.enabled", false);
@ -1928,7 +1926,7 @@ pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_sourc
pref("identity.fxaccounts.commands.missed.fetch_interval", 86400);
// Controls whether this client can send and receive "close tab"
// commands from other FxA clients
// commands from other FxA clients
pref("identity.fxaccounts.commands.remoteTabManagement.enabled", false);
// Note: when media.gmp-*.visible is true, provided we're running on a

Просмотреть файл

@ -1088,27 +1088,18 @@ var BrowserAddonUI = {
return { remove: result === 0, report: checkboxState.value };
},
async reportAddon(addonId, reportEntryPoint) {
async reportAddon(addonId, _reportEntryPoint) {
let addon = addonId && (await AddonManager.getAddonByID(addonId));
if (!addon) {
return;
}
// Do not open an additional about:addons tab if the abuse report should be
// opened in its own tab.
if (lazy.AbuseReporter.amoFormEnabled) {
const amoUrl = lazy.AbuseReporter.getAMOFormURL({ addonId });
window.openTrustedLinkIn(amoUrl, "tab", {
// Make sure the newly open tab is going to be focused, independently
// from general user prefs.
forceForeground: true,
});
return;
}
const win = await this.openAddonsMgr("addons://list/extension");
win.openAbuseReport({ addonId, reportEntryPoint });
const amoUrl = lazy.AbuseReporter.getAMOFormURL({ addonId });
window.openTrustedLinkIn(amoUrl, "tab", {
// Make sure the newly open tab is going to be focused, independently
// from general user prefs.
forceForeground: true,
});
},
async removeAddon(addonId) {

Просмотреть файл

@ -2,10 +2,6 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"ABUSE_REPORT_ENABLED",
@ -546,35 +542,6 @@ async function browseraction_contextmenu_report_extension_helper() {
useAddonManager: "temporary",
});
async function testReportDialog(viaUnifiedContextMenu) {
const reportDialogWindow = await BrowserTestUtils.waitForCondition(
() => AbuseReporter.getOpenDialog(),
"Wait for the abuse report dialog to have been opened"
);
const reportDialogParams = reportDialogWindow.arguments[0].wrappedJSObject;
is(
reportDialogParams.report.addon.id,
id,
"Abuse report dialog has the expected addon id"
);
is(
reportDialogParams.report.reportEntryPoint,
viaUnifiedContextMenu ? "unified_context_menu" : "toolbar_context_menu",
"Abuse report dialog has the expected reportEntryPoint"
);
info("Wait the report dialog to complete rendering");
await reportDialogParams.promiseReportPanel;
info("Close the report dialog");
reportDialogWindow.close();
is(
await reportDialogParams.promiseReport,
undefined,
"Report resolved as user cancelled when the window is closed"
);
}
async function testContextMenu(menuId, customizing) {
info(`Open browserAction context menu in ${menuId}`);
let menu = await openContextMenu(menuId, buttonId);
@ -591,54 +558,27 @@ async function browseraction_contextmenu_report_extension_helper() {
let aboutAddonsBrowser;
if (AbuseReporter.amoFormEnabled) {
const reportURL = Services.urlFormatter
.formatURLPref("extensions.abuseReport.amoFormURL")
.replace("%addonID%", id);
const reportURL = Services.urlFormatter
.formatURLPref("extensions.abuseReport.amoFormURL")
.replace("%addonID%", id);
const promiseReportTab = BrowserTestUtils.waitForNewTab(
gBrowser,
reportURL,
/* waitForLoad */ false,
// Expect it to be the next tab opened
/* waitForAnyTab */ false
);
await closeChromeContextMenu(menuId, reportExtension);
const reportTab = await promiseReportTab;
// Remove the report tab and expect the selected tab
// to become the about:addons tab.
BrowserTestUtils.removeTab(reportTab);
is(
gBrowser.selectedBrowser.currentURI.spec,
"about:blank",
"Expect about:addons tab to not have been opened (amoFormEnabled=true)"
);
} else {
// When running in customizing mode "about:addons" will load in a new tab,
// otherwise it will replace the existing blank tab.
const onceAboutAddonsTab = customizing
? BrowserTestUtils.waitForNewTab(gBrowser, "about:addons")
: BrowserTestUtils.waitForCondition(() => {
return gBrowser.currentURI.spec === "about:addons";
}, "Wait an about:addons tab to be opened");
await closeChromeContextMenu(menuId, reportExtension);
await onceAboutAddonsTab;
const browser = gBrowser.selectedBrowser;
is(
browser.currentURI.spec,
"about:addons",
"Got about:addons tab selected (amoFormEnabled=false)"
);
// Do not wait for the about:addons tab to be loaded if its
// document is already readyState==complete.
// This prevents intermittent timeout failures while running
// this test in optimized builds.
if (browser.contentDocument?.readyState != "complete") {
await BrowserTestUtils.browserLoaded(browser);
}
await testReportDialog(usingUnifiedContextMenu);
aboutAddonsBrowser = browser;
}
const promiseReportTab = BrowserTestUtils.waitForNewTab(
gBrowser,
reportURL,
/* waitForLoad */ false,
// Expect it to be the next tab opened
/* waitForAnyTab */ false
);
await closeChromeContextMenu(menuId, reportExtension);
const reportTab = await promiseReportTab;
// Remove the report tab and expect the selected tab
// to become the about:addons tab.
BrowserTestUtils.removeTab(reportTab);
is(
gBrowser.selectedBrowser.currentURI.spec,
"about:blank",
"Expect about:addons tab to not have been opened"
);
// Close the new about:addons tab when running in customize mode,
// or cancel the abuse report if the about:addons page has been
@ -729,22 +669,7 @@ add_task(async function test_unified_extensions_ui() {
await browseraction_contextmenu_manage_extension_helper();
await browseraction_contextmenu_remove_extension_helper();
await test_no_toolbar_pinning_on_builtin_helper();
});
add_task(async function test_report_amoFormEnabled() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amoFormEnabled", true]],
});
await browseraction_contextmenu_report_extension_helper();
await SpecialPowers.popPrefEnv();
});
add_task(async function test_report_amoFormDisabled() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amoFormEnabled", false]],
});
await browseraction_contextmenu_report_extension_helper();
await SpecialPowers.popPrefEnv();
});
/**

Просмотреть файл

@ -5,10 +5,6 @@
requestLongerTimeout(2);
ChromeUtils.defineESModuleGetters(this, {
AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
});
const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
"resource://testing-common/EnterprisePolicyTesting.sys.mjs"
);
@ -31,21 +27,6 @@ const promiseExtensionUninstalled = extensionId => {
});
};
function waitClosedWindow(win) {
return new Promise(resolve => {
function onWindowClosed() {
if (win && !win.closed) {
// If a specific window reference has been passed, then check
// that the window is closed before resolving the promise.
return;
}
Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
resolve();
}
Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
});
}
function assertVisibleContextMenuItems(contextMenu, expected) {
let visibleItems = contextMenu.querySelectorAll(
":is(menuitem, menuseparator):not([hidden])"
@ -366,90 +347,33 @@ add_task(async function test_report_extension() {
// closed and about:addons is open with the "abuse report dialog".
const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
if (AbuseReporter.amoFormEnabled) {
const reportURL = Services.urlFormatter
.formatURLPref("extensions.abuseReport.amoFormURL")
.replace("%addonID%", extension.id);
const reportURL = Services.urlFormatter
.formatURLPref("extensions.abuseReport.amoFormURL")
.replace("%addonID%", extension.id);
const promiseReportTab = BrowserTestUtils.waitForNewTab(
gBrowser,
reportURL,
/* waitForLoad */ false,
// Do not expect it to be the next tab opened
/* waitForAnyTab */ true
);
contextMenu.activateItem(reportButton);
const [reportTab] = await Promise.all([promiseReportTab, hidden]);
// Remove the report tab and expect the selected tab
// to become the about:addons tab.
BrowserTestUtils.removeTab(reportTab);
if (AbuseReporter.amoFormEnabled) {
is(
gBrowser.selectedBrowser.currentURI.spec,
"about:blank",
"Expect about:addons tab to have not been opened (amoFormEnabled=true)"
);
} else {
is(
gBrowser.selectedBrowser.currentURI.spec,
"about:addons",
"Got about:addons tab selected (amoFormEnabled=false)"
);
}
return;
}
const abuseReportOpen = BrowserTestUtils.waitForCondition(
() => AbuseReporter.getOpenDialog(),
"wait for the abuse report dialog to have been opened"
const promiseReportTab = BrowserTestUtils.waitForNewTab(
gBrowser,
reportURL,
/* waitForLoad */ false,
// Do not expect it to be the next tab opened
/* waitForAnyTab */ true
);
contextMenu.activateItem(reportButton);
const [reportDialogWindow] = await Promise.all([abuseReportOpen, hidden]);
const reportDialogParams =
reportDialogWindow.arguments[0].wrappedJSObject;
const [reportTab] = await Promise.all([promiseReportTab, hidden]);
// Remove the report tab and expect the selected tab
// to become the about:addons tab.
BrowserTestUtils.removeTab(reportTab);
is(
reportDialogParams.report.addon.id,
extension.id,
"abuse report dialog has the expected addon id"
gBrowser.selectedBrowser.currentURI.spec,
"about:blank",
"Expect about:addons tab to have not been opened"
);
is(
reportDialogParams.report.reportEntryPoint,
"unified_context_menu",
"abuse report dialog has the expected reportEntryPoint"
);
let promiseClosedWindow = waitClosedWindow();
reportDialogWindow.close();
// Wait for the report dialog window to be completely closed
// (to prevent an intermittent failure due to a race between
// the dialog window being closed and the test tasks that follows
// opening the unified extensions button panel to not lose the
// focus and be suddently closed before the task has done with
// its assertions, see Bug 1782304).
await promiseClosedWindow;
});
}
const [ext] = createExtensions([{ name: "an extension" }]);
await ext.startup();
info("Test report with amoFormEnabled=true");
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amoFormEnabled", true]],
});
await runReportTest(ext);
await SpecialPowers.popPrefEnv();
info("Test report with amoFormEnabled=false");
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amoFormEnabled", false]],
});
await runReportTest(ext);
await SpecialPowers.popPrefEnv();
await ext.unload();
});

Просмотреть файл

@ -208,9 +208,6 @@ pref("dom.serviceWorkers.disable_open_click_delay", 5000);
// Enable WebShare support (bug 1402369)
pref("dom.webshare.enabled", true);
// The abuse report feature needs some UI that we do not have on mobile
pref("extensions.abuseReport.amWebAPI.enabled", false);
// Disable add-ons that are not installed by the user in all scopes by default (See the SCOPE
// constants in AddonManager.sys.mjs for values to use here, and Bug 1405528 for a rationale)
pref("extensions.autoDisableScopes", 15);

Просмотреть файл

@ -3721,11 +3721,6 @@ class WebExtensionTest : BaseSessionTest() {
mainSession.evaluateJS("typeof navigator.mozAddonManager") as String,
equalTo("object"),
)
assertThat(
"mozAddonManager.abuseReportPanelEnabled should be false",
mainSession.evaluateJS("navigator.mozAddonManager.abuseReportPanelEnabled") as Boolean,
equalTo(false),
)
// Install an add-on, then assert results got from `mozAddonManager.getAddonByID()`.
var addonId = ""

Просмотреть файл

@ -1882,12 +1882,7 @@ pref("services.settings.poll_interval", 86400); // 24H
pref("services.common.uptake.sampleRate", 1); // 1%
pref("extensions.abuseReport.enabled", false);
// Whether abuse report originated from AMO should use the Firefox integrated dialog.
pref("extensions.abuseReport.amWebAPI.enabled", false);
pref("extensions.abuseReport.url", "https://services.addons.mozilla.org/api/v4/abuse/report/addon/");
pref("extensions.abuseReport.amoDetailsURL", "https://services.addons.mozilla.org/api/v4/addons/addon/");
// Whether Firefox integrated abuse reporting feature should be opening the new abuse report form hosted on AMO.
pref("extensions.abuseReport.amoFormEnabled", false);
pref("extensions.abuseReport.amoFormURL", "https://addons.mozilla.org/%LOCALE%/%APP%/feedback/addon/%addonID%/");
// Blocklist preferences

Просмотреть файл

@ -294,33 +294,6 @@ addonsManager:
record_in_processes: ["main"]
bug_numbers: [1433335, 1515697, 1523641, 1549770, 1590736, 1630596, 1672570, 1714251, 1749878, 1781974, 1817100, 1861295]
release_channel_collection: opt-out
report:
description: >
An abuse report submitted by a user for a given extension. The object of the event
represent the report entry point, the value is the id of the addon being reported.
objects:
- amo
- menu
- toolbar_context_menu
- unified_context_menu
- uninstall
extra_keys:
addon_type: >
The type of the add-on being reported (missing on ERROR_ADDON_NOT_FOUND, ERROR_AMODETAILS_NOTFOUND
and ERROR_AMODETAILS_FAILURE).
error_type: >
AbuseReport Error Type (included in case of submission failures). The error types include
ERROR_ABORTED_SUBMIT, ERROR_ADDON_NOT_FOUND, ERROR_CLIENT, ERROR_NETWORK, ERROR_UNKNOWN,
ERROR_RECENT_SUBMIT, ERROR_SERVER, ERROR_AMODETAILS_NOTFOUND, ERROR_AMODETAILS_FAILURE.
notification_emails: ["addons-dev-internal@mozilla.com"]
expiry_version: "132"
products:
- "firefox"
- "fennec"
- "thunderbird"
record_in_processes: ["main"]
bug_numbers: [1544927, 1580561, 1590736, 1630596, 1672570, 1714251, 1749878, 1780746, 1781974, 1817100, 1861295]
release_channel_collection: opt-out
reportSuspiciousSite:
description: >
Sent when a user clicks "Report Suspicious Site" on the dropdown menu of the third-

Просмотреть файл

@ -1,116 +0,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/.
# Localized string used as the dialog window title.
# "Report" is a noun in this case, "Report for AddonName".
#
# Variables:
# $addon-name (string) - Name of the add-on being reported
abuse-report-dialog-title = Report for { $addon-name }
abuse-report-title-extension = Report This Extension to { -vendor-short-name }
abuse-report-title-sitepermission = Report This Site Permissions add-on to { -vendor-short-name }
abuse-report-title-theme = Report This Theme to { -vendor-short-name }
abuse-report-subtitle = Whats the issue?
# Variables:
# $author-name (string) - Name of the add-on author
abuse-report-addon-authored-by = by <a data-l10n-name="author-name">{ $author-name }</a>
abuse-report-learnmore-intro = Unsure what issue to select?
abuse-report-learnmore-link = Learn more about reporting extensions and themes
abuse-report-submit-description = Describe the problem (optional)
abuse-report-textarea =
.placeholder = Its easier for us to address a problem if we have specifics. Please describe what youre experiencing. Thank you for helping us keep the web healthy.
abuse-report-submit-note =
Note: Dont include personal information (such as name, email address, phone number, physical address).
{ -vendor-short-name } keeps a permanent record of these reports.
## Panel buttons.
abuse-report-cancel-button = Cancel
abuse-report-next-button = Next
abuse-report-goback-button = Go back
abuse-report-submit-button = Submit
## Message bars descriptions.
##
## Variables:
## $addon-name (string) - Name of the add-on
abuse-report-messagebar-aborted2 =
.message = Report for { $addon-name } canceled.
abuse-report-messagebar-submitting2 =
.message = Sending report for { $addon-name }.
abuse-report-messagebar-submitted2 =
.message = Thank you for submitting a report. Do you want to remove { $addon-name }?
abuse-report-messagebar-submitted-noremove2 =
.message = Thank you for submitting a report.
abuse-report-messagebar-removed-extension2 =
.message = Thank you for submitting a report. Youve removed the extension { $addon-name }.
abuse-report-messagebar-removed-sitepermission2 =
.message = Thank you for submitting a report. Youve removed the Site Permissions add-on { $addon-name }.
abuse-report-messagebar-removed-theme2 =
.message = Thank you for submitting a report. Youve removed the theme { $addon-name }.
abuse-report-messagebar-error2 =
.message = There was an error sending the report for { $addon-name }.
abuse-report-messagebar-error-recent-submit2 =
.message = The report for { $addon-name } wasnt sent because another report was submitted recently.
## Message bars actions.
abuse-report-messagebar-action-remove-extension = Yes, Remove It
abuse-report-messagebar-action-keep-extension = No, Ill Keep It
abuse-report-messagebar-action-remove-sitepermission = Yes, Remove It
abuse-report-messagebar-action-keep-sitepermission = No, Ill Keep It
abuse-report-messagebar-action-remove-theme = Yes, Remove It
abuse-report-messagebar-action-keep-theme = No, Ill Keep It
abuse-report-messagebar-action-retry = Retry
abuse-report-messagebar-action-cancel = Cancel
## Abuse report reasons (optionally paired with related examples and/or suggestions)
abuse-report-damage-reason-v2 = It damaged my computer or compromised my data
abuse-report-damage-example = Example: Injected malware or stole data
abuse-report-spam-reason-v2 = It contains spam or inserts unwanted advertising
abuse-report-spam-example = Example: Insert ads on webpages
abuse-report-settings-reason-v2 = It changed my search engine, homepage, or new tab without informing or asking me
abuse-report-settings-suggestions = Before reporting the extension, you can try changing your settings:
abuse-report-settings-suggestions-search = Change your default search settings
abuse-report-settings-suggestions-homepage = Change your homepage and new tab
abuse-report-deceptive-reason-v2 = It claims to be something its not
abuse-report-deceptive-example = Example: Misleading description or imagery
abuse-report-broken-reason-extension-v2 = It doesnt work, breaks websites, or slows down { -brand-product-name }
abuse-report-broken-reason-sitepermission-v2 = It doesnt work, breaks websites, or slows down { -brand-product-name }
abuse-report-broken-reason-theme-v2 = It doesnt work or breaks browser display
abuse-report-broken-example =
Example: Features are slow, hard to use, or dont work; parts of websites wont load or look unusual
abuse-report-broken-suggestions-extension =
It sounds like youve identified a bug. In addition to submitting a report here, the best way
to get a functionality issue resolved is to contact the extension developer.
<a data-l10n-name="support-link">Visit the extensions website</a> to get the developer information.
abuse-report-broken-suggestions-sitepermission =
It sounds like youve identified a bug. In addition to submitting a report here, the best way
to get a functionality issue resolved is to contact the website developer.
<a data-l10n-name="support-link">Visit the website</a> to get the developer information.
abuse-report-broken-suggestions-theme =
It sounds like youve identified a bug. In addition to submitting a report here, the best way
to get a functionality issue resolved is to contact the theme developer.
<a data-l10n-name="support-link">Visit the themes website</a> to get the developer information.
abuse-report-policy-reason-v2 = It contains hateful, violent, or illegal content
abuse-report-policy-suggestions =
Note: Copyright and trademark issues must be reported in a separate process.
<a data-l10n-name="report-infringement-link">Use these instructions</a> to
report the problem.
abuse-report-unwanted-reason-v2 = I never wanted it and dont know how to get rid of it
abuse-report-unwanted-example = Example: An application installed it without my permission
abuse-report-other-reason = Something else

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -26,7 +26,6 @@ const MOZ_COMPATIBILITY_NIGHTLY = ![
const INTL_LOCALES_CHANGED = "intl:app-locales-changed";
const PREF_AMO_ABUSEREPORT = "extensions.abuseReport.amWebAPI.enabled";
const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
@ -81,7 +80,6 @@ var AsyncShutdown = realAsyncShutdown;
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
Extension: "resource://gre/modules/Extension.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
@ -3628,54 +3626,6 @@ var AddonManagerInternal = {
}
}
},
async addonReportAbuse(target, id) {
if (!Services.prefs.getBoolPref(PREF_AMO_ABUSEREPORT, false)) {
return Promise.reject({
message: "amWebAPI reportAbuse not supported",
});
}
let existingDialog = lazy.AbuseReporter.getOpenDialog();
if (existingDialog) {
existingDialog.close();
}
const dialog = await lazy.AbuseReporter.openDialog(
id,
"amo",
target
).catch(err => {
Cu.reportError(err);
return Promise.reject({
message: "Error creating abuse report",
});
});
return dialog.promiseReport.then(
async report => {
if (!report) {
return false;
}
await report.submit().catch(err => {
Cu.reportError(err);
return Promise.reject({
message: "Error submitting abuse report",
});
});
return true;
},
err => {
Cu.reportError(err);
dialog.close();
return Promise.reject({
message: "Error creating abuse report",
});
}
);
},
},
};
@ -5250,40 +5200,6 @@ AMTelemetry = {
);
},
/**
* Record an event on abuse report submissions.
*
* @params {object} opts
* @params {string} opts.addonId
* The id of the addon being reported.
* @params {string} [opts.addonType]
* The type of the addon being reported (only present for an existing
* addonId).
* @params {string} [opts.errorType]
* The AbuseReport errorType for a submission failure.
* @params {string} opts.reportEntryPoint
* The entry point of the abuse report.
*/
recordReportEvent({ addonId, addonType, errorType, reportEntryPoint }) {
this.recordEvent({
method: "report",
object: reportEntryPoint,
value: addonId,
extra: this.formatExtraVars({
addon_type: addonType,
error_type: errorType,
}),
});
Glean.addonsManager.report.record(
this.formatExtraVars({
addon_id: addonId,
addon_type: addonType,
entry_point: reportEntryPoint,
error_type: errorType,
})
);
},
/**
* @params {object} opts
* @params {nsIURI} opts.displayURI

Просмотреть файл

@ -2,17 +2,6 @@
* 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";
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"AMO_ABUSEREPORT",
"extensions.abuseReport.amWebAPI.enabled",
false
);
const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
const MSG_INSTALL_EVENT = "WebAPIInstallEvent";
@ -255,14 +244,6 @@ export class WebAPI extends APIObject {
});
}
reportAbuse(id) {
return this._apiTask("addonReportAbuse", [id]);
}
get abuseReportPanelEnabled() {
return lazy.AMO_ABUSEREPORT;
}
eventListenerAdded() {
if (this.listenerCount == 0) {
this.broker.setAddonListener(data => {

Просмотреть файл

@ -185,8 +185,7 @@ search-addons > search-textbox {
max-width: var(--section-width);
}
global-warnings,
#abuse-reports-messages {
global-warnings {
margin-inline-start: var(--main-margin-start);
max-width: var(--section-width);
}

Просмотреть файл

@ -30,7 +30,6 @@
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="toolkit/about/aboutAddons.ftl" />
<link rel="localization" href="toolkit/about/abuseReports.ftl" />
<!-- Defer scripts so all the templates are loaded by the time they run. -->
<script
@ -163,13 +162,6 @@
></addon-page-header>
<addon-page-options id="page-options"></addon-page-options>
<message-bar-stack
id="abuse-reports-messages"
reverse
max-message-bar-count="3"
>
</message-bar-stack>
<div id="main"></div>
</div>
</div>

Просмотреть файл

@ -1,213 +0,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/. -->
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
<link
rel="stylesheet"
href="chrome://mozapps/content/extensions/aboutaddons.css"
/>
<link
rel="stylesheet"
href="chrome://mozapps/content/extensions/abuse-report-panel.css"
/>
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="toolkit/about/aboutAddons.ftl" />
<link rel="localization" href="toolkit/about/abuseReports.ftl" />
<script
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
<script
type="module"
src="chrome://global/content/elements/moz-support-link.mjs"
></script>
<script
defer
src="chrome://mozapps/content/extensions/abuse-report-panel.js"
></script>
</head>
<body>
<addon-abuse-report></addon-abuse-report>
<!-- WebComponents Templates -->
<template id="tmpl-modal">
<div class="modal-overlay-outer"></div>
<div class="modal-panel-container"></div>
</template>
<template id="tmpl-abuse-report">
<form class="addon-abuse-report" onsubmit="return false;">
<div class="abuse-report-header">
<img class="card-heading-icon addon-icon" />
<div class="card-contents">
<span class="addon-name"></span>
<span
class="addon-author-box"
data-l10n-args='{"author-name": "author placeholder"}'
data-l10n-id="abuse-report-addon-authored-by"
>
<a
data-l10n-name="author-name"
class="author"
href="#"
target="_blank"
></a>
</span>
</div>
</div>
<button class="abuse-report-close-icon" type="button"></button>
<div class="abuse-report-contents">
<abuse-report-reasons-panel></abuse-report-reasons-panel>
<abuse-report-submit-panel hidden></abuse-report-submit-panel>
</div>
<div class="abuse-report-buttons">
<moz-button-group class="abuse-report-reasons-buttons">
<button
class="abuse-report-cancel"
type="button"
data-l10n-id="abuse-report-cancel-button"
></button>
<button
class="primary abuse-report-next"
type="button"
data-l10n-id="abuse-report-next-button"
></button>
</moz-button-group>
<moz-button-group class="abuse-report-submit-buttons" hidden>
<button
class="abuse-report-goback"
type="button"
data-l10n-id="abuse-report-goback-button"
></button>
<button
class="primary abuse-report-submit"
type="button"
data-l10n-id="abuse-report-submit-button"
></button>
</moz-button-group>
</div>
</form>
</template>
<template id="tmpl-reasons-panel">
<h2 class="abuse-report-title"></h2>
<hr />
<p class="abuse-report-subtitle" data-l10n-id="abuse-report-subtitle"></p>
<ul class="abuse-report-reasons">
<li is="abuse-report-reason-listitem" report-reason="other"></li>
</ul>
<p>
<span data-l10n-id="abuse-report-learnmore-intro"></span>
<a
is="moz-support-link"
target="_blank"
support-page="reporting-extensions-and-themes-abuse"
data-l10n-id="abuse-report-learnmore-link"
>
</a>
</p>
</template>
<template id="tmpl-submit-panel">
<h2 class="abuse-reason-title"></h2>
<abuse-report-reason-suggestions></abuse-report-reason-suggestions>
<hr />
<p
class="abuse-report-subtitle"
data-l10n-id="abuse-report-submit-description"
></p>
<textarea name="message" data-l10n-id="abuse-report-textarea"></textarea>
<p class="abuse-report-note" data-l10n-id="abuse-report-submit-note"></p>
</template>
<template id="tmpl-reason-listitem">
<label>
<input type="radio" name="reason" class="radio" />
<span class="reason-description"></span>
<span hidden class="abuse-report-note reason-example"></span>
</label>
</template>
<template id="tmpl-suggestions-settings">
<p data-l10n-id="abuse-report-settings-suggestions"></p>
<p></p>
<ul>
<li>
<a
is="moz-support-link"
target="_blank"
data-l10n-id="abuse-report-settings-suggestions-search"
support-page="prefs-search"
>
</a>
</li>
<li>
<a
is="moz-support-link"
target="_blank"
data-l10n-id="abuse-report-settings-suggestions-homepage"
support-page="prefs-homepage"
>
</a>
</li>
</ul>
</template>
<template id="tmpl-suggestions-policy">
<p data-l10n-id="abuse-report-policy-suggestions">
<a
class="abuse-policy-learnmore"
target="_blank"
data-l10n-name="report-infringement-link"
>
</a>
</p>
</template>
<template id="tmpl-suggestions-broken-extension">
<p data-l10n-id="abuse-report-broken-suggestions-extension">
<a
class="extension-support-link"
target="_blank"
data-l10n-name="support-link"
>
</a>
</p>
<p></p
></template>
<template id="tmpl-suggestions-broken-theme">
<p data-l10n-id="abuse-report-broken-suggestions-theme">
<a
class="extension-support-link"
target="_blank"
data-l10n-name="support-link"
>
</a>
</p>
<p></p
></template>
<template id="tmpl-suggestions-broken-sitepermission">
<p data-l10n-id="abuse-report-broken-suggestions-sitepermission">
<a
class="extension-support-link"
target="_blank"
data-l10n-name="support-link"
>
</a>
</p>
<p></p
></template>
</body>
</html>

Просмотреть файл

@ -1,181 +0,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/. */
/* Abuse Reports card */
:root {
--close-icon-url: url("chrome://global/skin/icons/close.svg");
--close-icon-size: 20px;
--modal-panel-min-width: 60%;
--modal-panel-margin-top: 36px;
--modal-panel-margin-bottom: 36px;
--modal-panel-margin: 20%;
--modal-panel-padding: 40px;
--line-height: 20px;
--textarea-height: 220px;
--listitem-padding-bottom: 14px;
--list-radio-column-size: 28px;
--note-font-size: 14px;
--note-font-weight: 400;
--subtitle-font-size: 16px;
--subtitle-font-weight: 600;
}
/* Ensure that the document (embedded in the XUL about:addons using a
XUL browser) has a transparent background */
html {
background-color: transparent;
}
.modal-overlay-outer {
background: var(--grey-90-a60);
width: 100%;
height: 100%;
position: fixed;
z-index: -1;
}
.modal-panel-container {
padding-top: var(--modal-panel-margin-top);
padding-bottom: var(--modal-panel-margin-bottom);
padding-left: var(--modal-panel-margin);
padding-right: var(--modal-panel-margin);
}
.addon-abuse-report {
min-width: var(--modal-panel-min-width);
padding: var(--modal-panel-padding);
display: flex;
flex-direction: column;
position: relative;
}
.addon-abuse-report:hover {
/* Avoid the card box highlighting on hover. */
box-shadow: none;
}
.abuse-report-close-icon {
/* position the close button in the panel upper-right corner */
position: absolute;
top: 12px;
inset-inline-end: 16px;
}
button.abuse-report-close-icon {
background: var(--close-icon-url) no-repeat center center;
-moz-context-properties: fill;
color: inherit !important;
fill: currentColor;
min-width: auto;
min-height: auto;
width: var(--close-icon-size);
height: var(--close-icon-size);
margin: 0;
padding: 0;
}
button.abuse-report-close-icon:hover {
fill-opacity: 0.1;
}
button.abuse-report-close-icon:hover:active {
fill-opacity: 0.2;
}
.abuse-report-header {
display: flex;
flex-direction: row;
}
.abuse-report-contents,
.abuse-report-contents > hr {
width: 100%;
}
.abuse-report-note {
color: var(--text-color-deemphasized);
font-size: var(--note-font-size);
font-weight: var(--note-font-weight);
line-height: var(--line-height);
}
.abuse-report-subtitle {
font-size: var(--subtitle-font-size);
font-weight: var(--subtitle-font-weight);
line-height: var(--line-height);
}
ul.abuse-report-reasons {
list-style-type: none;
padding-inline-start: 0;
}
ul.abuse-report-reasons > li {
display: flex;
padding-bottom: var(--listitem-padding-bottom);
}
ul.abuse-report-reasons > li > label {
display: grid;
grid-template-columns: var(--list-radio-column-size) auto;
grid-template-rows: 50% auto;
width: 100%;
line-height: var(--line-height);
font-size: var(--subtitle-font-size);
font-weight: var(--note-font-weight);
margin-inline-start: 4px;
}
ul.abuse-report-reasons > li > label > [type="radio"] {
grid-column: 1;
}
ul.abuse-report-reasons > li > label > span {
grid-column: 2;
}
abuse-report-submit-panel textarea {
width: 100%;
height: var(--textarea-height);
resize: none;
box-sizing: border-box;
}
/* Adapt styles for the panel opened in its own dialog window */
html.dialog-window {
background-color: var(--in-content-box-background);
height: 100%;
min-width: 740px;
}
html.dialog-window body {
overflow: hidden;
min-height: 100%;
display: flex;
flex-direction: column;
}
html.dialog-window .abuse-report-close-icon {
display: none;
}
html.dialog-window addon-abuse-report {
flex-grow: 1;
display: flex;
/* Ensure that the dialog window starts from a reasonable initial size */
--modal-panel-min-width: 700px;
}
html.dialog-window addon-abuse-report form {
display: flex;
}
html.dialog-window addon-abuse-report form .abuse-report-contents {
flex-grow: 1;
}

Просмотреть файл

@ -1,873 +0,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/. */
/* eslint max-len: ["error", 80] */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
});
const IS_DIALOG_WINDOW = window.arguments && window.arguments.length;
let openWebLink = IS_DIALOG_WINDOW
? window.arguments[0].wrappedJSObject.openWebLink
: url => {
window.windowRoot.ownerGlobal.openWebLinkIn(url, "tab", {
relatedToCurrent: true,
});
};
const showOnAnyType = () => false;
const hideOnAnyType = () => true;
const hideOnAddonTypes = hideForTypes => {
return addonType => hideForTypes.includes(addonType);
};
// The reasons string used as a key in this Map is expected to stay in sync
// with the reasons string used in the "abuseReports.ftl" locale file and
// the suggestions templates included in abuse-report-frame.html.
const ABUSE_REASONS = (window.ABUSE_REPORT_REASONS = {
damage: {
isExampleHidden: showOnAnyType,
isReasonHidden: hideOnAddonTypes(["theme"]),
},
spam: {
isExampleHidden: showOnAnyType,
isReasonHidden: hideOnAddonTypes(["sitepermission"]),
},
settings: {
hasSuggestions: true,
isExampleHidden: hideOnAnyType,
isReasonHidden: hideOnAddonTypes(["theme", "sitepermission"]),
},
deceptive: {
isExampleHidden: showOnAnyType,
isReasonHidden: hideOnAddonTypes(["sitepermission"]),
},
broken: {
hasAddonTypeL10nId: true,
hasAddonTypeSuggestionTemplate: true,
hasSuggestions: true,
isExampleHidden: hideOnAddonTypes(["theme"]),
isReasonHidden: showOnAnyType,
requiresSupportURL: true,
},
policy: {
hasSuggestions: true,
isExampleHidden: hideOnAnyType,
isReasonHidden: hideOnAddonTypes(["sitepermission"]),
},
unwanted: {
isExampleHidden: showOnAnyType,
isReasonHidden: hideOnAddonTypes(["theme"]),
},
other: {
isExampleHidden: hideOnAnyType,
isReasonHidden: showOnAnyType,
},
});
// Maps the reason id to the last version of the related fluent id.
// NOTE: when changing the localized string, increase the `-vN` suffix
// in the abuseReports.ftl fluent file and update this mapping table.
const REASON_L10N_STRING_MAPPING = {
"abuse-report-damage-reason": "abuse-report-damage-reason-v2",
"abuse-report-spam-reason": "abuse-report-spam-reason-v2",
"abuse-report-settings-reason": "abuse-report-settings-reason-v2",
"abuse-report-deceptive-reason": "abuse-report-deceptive-reason-v2",
"abuse-report-broken-reason-extension":
"abuse-report-broken-reason-extension-v2",
"abuse-report-broken-reason-sitepermission":
"abuse-report-broken-reason-sitepermission-v2",
"abuse-report-broken-reason-theme": "abuse-report-broken-reason-theme-v2",
"abuse-report-policy-reason": "abuse-report-policy-reason-v2",
"abuse-report-unwanted-reason": "abuse-report-unwanted-reason-v2",
};
function getReasonL10nId(reason, addonType) {
let reasonId = `abuse-report-${reason}-reason`;
// Special case reasons that have a addonType-specific
// l10n id.
if (ABUSE_REASONS[reason].hasAddonTypeL10nId) {
reasonId += `-${addonType}`;
}
// Map the reason to the corresponding versionized fluent string, using the
// mapping table above, if available.
return REASON_L10N_STRING_MAPPING[reasonId] || reasonId;
}
function getSuggestionsTemplate({ addonType, reason, supportURL }) {
const reasonInfo = ABUSE_REASONS[reason];
if (
!addonType ||
!reasonInfo.hasSuggestions ||
(reasonInfo.requiresSupportURL && !supportURL)
) {
return null;
}
let templateId = `tmpl-suggestions-${reason}`;
// Special case reasons that have a addonType-specific
// suggestion template.
if (reasonInfo.hasAddonTypeSuggestionTemplate) {
templateId += `-${addonType}`;
}
return document.getElementById(templateId);
}
// Map of the learnmore links metadata, keyed by link element class.
const LEARNMORE_LINKS = {
".abuse-policy-learnmore": {
baseURL: "https://www.mozilla.org/%LOCALE%/",
path: "about/legal/report-infringement/",
},
};
// Format links that match the selector in the LEARNMORE_LINKS map
// found in a given container element.
function formatLearnMoreURLs(containerEl) {
for (const [linkClass, linkInfo] of Object.entries(LEARNMORE_LINKS)) {
for (const element of containerEl.querySelectorAll(linkClass)) {
const baseURL = Services.urlFormatter.formatURL(linkInfo.baseURL);
element.href = baseURL + linkInfo.path;
}
}
}
// Define a set of getters from a Map<propertyName, selector>.
function defineElementSelectorsGetters(object, propsMap) {
const props = Object.entries(propsMap).reduce((acc, entry) => {
const [name, selector] = entry;
acc[name] = { get: () => object.querySelector(selector) };
return acc;
}, {});
Object.defineProperties(object, props);
}
// Define a set of properties getters and setters for a
// Map<propertyName, attributeName>.
function defineElementAttributesProperties(object, propsMap) {
const props = Object.entries(propsMap).reduce((acc, entry) => {
const [name, attr] = entry;
acc[name] = {
get: () => object.getAttribute(attr),
set: value => {
object.setAttribute(attr, value);
},
};
return acc;
}, {});
Object.defineProperties(object, props);
}
// Return an object with properties associated to elements
// found using the related selector in the propsMap.
function getElements(containerEl, propsMap) {
return Object.entries(propsMap).reduce((acc, entry) => {
const [name, selector] = entry;
let elements = containerEl.querySelectorAll(selector);
acc[name] = elements.length > 1 ? elements : elements[0];
return acc;
}, {});
}
function dispatchCustomEvent(el, eventName, detail) {
el.dispatchEvent(new CustomEvent(eventName, { detail }));
}
// This WebComponent extends the li item to represent an abuse report reason
// and it is responsible for:
// - embedding a photon styled radio buttons
// - localizing the reason list item
// - optionally embedding a localized example, positioned
// below the reason label, and adjusts the item height
// accordingly
class AbuseReasonListItem extends HTMLLIElement {
constructor() {
super();
defineElementAttributesProperties(this, {
addonType: "addon-type",
reason: "report-reason",
checked: "checked",
});
}
connectedCallback() {
this.update();
}
async update() {
if (this.reason !== "other" && !this.addonType) {
return;
}
const { reason, checked, addonType } = this;
this.textContent = "";
const content = document.importNode(this.template.content, true);
if (reason) {
const reasonId = `abuse-reason-${reason}`;
const reasonInfo = ABUSE_REASONS[reason] || {};
const { labelEl, descriptionEl, radioEl } = getElements(content, {
labelEl: "label",
descriptionEl: ".reason-description",
radioEl: "input[type=radio]",
});
labelEl.setAttribute("for", reasonId);
radioEl.id = reasonId;
radioEl.value = reason;
radioEl.checked = !!checked;
// This reason has a different localized description based on the
// addon type.
document.l10n.setAttributes(
descriptionEl,
getReasonL10nId(reason, addonType)
);
// Show the reason example if supported for the addon type.
if (!reasonInfo.isExampleHidden(addonType)) {
const exampleEl = content.querySelector(".reason-example");
document.l10n.setAttributes(
exampleEl,
`abuse-report-${reason}-example`
);
exampleEl.hidden = false;
}
}
formatLearnMoreURLs(content);
this.appendChild(content);
}
get template() {
return document.getElementById("tmpl-reason-listitem");
}
}
// This WebComponents implements the first step of the abuse
// report submission and embeds a randomized reasons list.
class AbuseReasonsPanel extends HTMLElement {
constructor() {
super();
defineElementAttributesProperties(this, {
addonType: "addon-type",
});
}
connectedCallback() {
this.update();
}
update() {
if (!this.isConnected || !this.addonType) {
return;
}
const { addonType } = this;
this.textContent = "";
const content = document.importNode(this.template.content, true);
const { titleEl, listEl } = getElements(content, {
titleEl: ".abuse-report-title",
listEl: "ul.abuse-report-reasons",
});
// Change the title l10n-id if the addon type is theme.
document.l10n.setAttributes(titleEl, `abuse-report-title-${addonType}`);
// Create the randomized list of reasons.
const reasons = Object.keys(ABUSE_REASONS)
.filter(reason => reason !== "other")
.sort(() => Math.random() - 0.5);
for (const reason of reasons) {
const reasonInfo = ABUSE_REASONS[reason];
if (!reasonInfo || reasonInfo.isReasonHidden(addonType)) {
// Skip an extension only reason while reporting a theme.
continue;
}
const item = document.createElement("li", {
is: "abuse-report-reason-listitem",
});
item.reason = reason;
item.addonType = addonType;
listEl.prepend(item);
}
listEl.firstElementChild.checked = true;
formatLearnMoreURLs(content);
this.appendChild(content);
}
get template() {
return document.getElementById("tmpl-reasons-panel");
}
}
// This WebComponent is responsible for the suggestions, which are:
// - generated based on a template keyed by abuse report reason
// - localized by assigning fluent ids generated from the abuse report reason
// - learn more and extension support url are then generated when the
// specific reason expects it
class AbuseReasonSuggestions extends HTMLElement {
constructor() {
super();
defineElementAttributesProperties(this, {
extensionSupportURL: "extension-support-url",
reason: "report-reason",
});
}
update() {
const { addonType, extensionSupportURL, reason } = this;
this.textContent = "";
let template = getSuggestionsTemplate({
addonType,
reason,
supportURL: extensionSupportURL,
});
if (template) {
let content = document.importNode(template.content, true);
formatLearnMoreURLs(content);
let extSupportLink = content.querySelector("a.extension-support-link");
if (extSupportLink) {
extSupportLink.href = extensionSupportURL;
}
this.appendChild(content);
this.hidden = false;
} else {
this.hidden = true;
}
}
}
// This WebComponents implements the last step of the abuse report submission.
class AbuseSubmitPanel extends HTMLElement {
constructor() {
super();
defineElementAttributesProperties(this, {
addonType: "addon-type",
reason: "report-reason",
extensionSupportURL: "extensionSupportURL",
});
defineElementSelectorsGetters(this, {
_textarea: "textarea",
_title: ".abuse-reason-title",
_suggestions: "abuse-report-reason-suggestions",
});
}
connectedCallback() {
this.render();
}
render() {
this.textContent = "";
this.appendChild(document.importNode(this.template.content, true));
}
update() {
if (!this.isConnected || !this.addonType) {
return;
}
const { addonType, reason, _suggestions, _title } = this;
document.l10n.setAttributes(_title, getReasonL10nId(reason, addonType));
_suggestions.reason = reason;
_suggestions.addonType = addonType;
_suggestions.extensionSupportURL = this.extensionSupportURL;
_suggestions.update();
}
clear() {
this._textarea.value = "";
}
get template() {
return document.getElementById("tmpl-submit-panel");
}
}
// This WebComponent provides the abuse report
class AbuseReport extends HTMLElement {
constructor() {
super();
this._report = null;
defineElementSelectorsGetters(this, {
_form: "form",
_textarea: "textarea",
_radioCheckedReason: "[type=radio]:checked",
_reasonsPanel: "abuse-report-reasons-panel",
_submitPanel: "abuse-report-submit-panel",
_reasonsPanelButtons: ".abuse-report-reasons-buttons",
_submitPanelButtons: ".abuse-report-submit-buttons",
_iconClose: ".abuse-report-close-icon",
_btnNext: "button.abuse-report-next",
_btnCancel: "button.abuse-report-cancel",
_btnGoBack: "button.abuse-report-goback",
_btnSubmit: "button.abuse-report-submit",
_addonAuthorContainer: ".abuse-report-header .addon-author-box",
_addonIconElement: ".abuse-report-header img.addon-icon",
_addonNameElement: ".abuse-report-header .addon-name",
_linkAddonAuthor: ".abuse-report-header .addon-author-box a.author",
});
}
connectedCallback() {
this.render();
this.addEventListener("click", this);
// Start listening to keydown events (to close the modal
// when Escape has been pressed and to handling the keyboard
// navigation).
document.addEventListener("keydown", this);
}
disconnectedCallback() {
this.textContent = "";
this.removeEventListener("click", this);
document.removeEventListener("keydown", this);
}
handleEvent(evt) {
if (!this.isConnected || !this.addon) {
return;
}
switch (evt.type) {
case "keydown":
if (evt.key === "Escape") {
// Prevent Esc to close the panel if the textarea is
// empty.
if (this.message && !this._submitPanel.hidden) {
return;
}
this.cancel();
}
if (!IS_DIALOG_WINDOW) {
// Workaround keyboard navigation issues when
// the panel is running in its own dialog window.
this.handleKeyboardNavigation(evt);
}
break;
case "click":
if (evt.target === this._iconClose || evt.target === this._btnCancel) {
// NOTE: clear the focus on the clicked element to ensure that
// -moz-focusring pseudo class is not still set on the element
// when the panel is going to be shown again (See Bug 1560949).
evt.target.blur();
this.cancel();
}
if (evt.target === this._btnNext) {
this.switchToSubmitMode();
}
if (evt.target === this._btnGoBack) {
this.switchToListMode();
}
if (evt.target === this._btnSubmit) {
this.submit();
}
if (evt.target.localName === "a") {
evt.preventDefault();
evt.stopPropagation();
const url = evt.target.getAttribute("href");
// Ignore if url is empty.
if (url) {
openWebLink(url);
}
}
break;
}
}
handleKeyboardNavigation(evt) {
if (
evt.keyCode !== evt.DOM_VK_TAB ||
evt.altKey ||
evt.controlKey ||
evt.metaKey
) {
return;
}
const fm = Services.focus;
const backward = evt.shiftKey;
const isFirstFocusableElement = el => {
// Also consider the document body as a valid first focusable element.
if (el === document.body) {
return true;
}
// XXXrpl unfortunately there is no way to get the first focusable element
// without asking the focus manager to move focus to it (similar strategy
// is also being used in about:prefereces subdialog.js).
const rv = el == fm.moveFocus(window, null, fm.MOVEFOCUS_FIRST, 0);
fm.setFocus(el, 0);
return rv;
};
// If the focus is exiting the panel while navigating
// backward, focus the previous element sibling on the
// Firefox UI.
if (backward && isFirstFocusableElement(evt.target)) {
evt.preventDefault();
evt.stopImmediatePropagation();
const chromeWin = window.windowRoot.ownerGlobal;
Services.focus.moveFocus(
chromeWin,
null,
Services.focus.MOVEFOCUS_BACKWARD,
Services.focus.FLAG_BYKEY
);
}
}
render() {
this.textContent = "";
const formTemplate = document.importNode(this.template.content, true);
if (IS_DIALOG_WINDOW) {
this.appendChild(formTemplate);
} else {
// Append the report form inside a modal overlay when the report panel
// is a sub-frame of the about:addons tab.
const modalTemplate = document.importNode(
this.modalTemplate.content,
true
);
this.appendChild(modalTemplate);
this.querySelector(".modal-panel-container").appendChild(formTemplate);
// Add the card styles to the form.
this.querySelector("form").classList.add("card");
}
}
async update() {
if (!this.addon) {
return;
}
const {
addonId,
addonType,
_addonAuthorContainer,
_addonIconElement,
_addonNameElement,
_linkAddonAuthor,
_reasonsPanel,
_submitPanel,
} = this;
// Ensure that the first step of the abuse submission is the one
// currently visible.
this.switchToListMode();
// Cancel the abuse report if the addon is not an extension or theme.
if (!AbuseReporter.isSupportedAddonType(addonType)) {
Cu.reportError(
new Error(
`Closing abuse report panel on unexpected addon type: ${addonType}`
)
);
this.cancel();
return;
}
_addonNameElement.textContent = this.addonName;
if (this.authorName) {
_linkAddonAuthor.href = this.authorURL || this.homepageURL;
_linkAddonAuthor.textContent = this.authorName;
document.l10n.setAttributes(
_linkAddonAuthor.parentNode,
"abuse-report-addon-authored-by",
{ "author-name": this.authorName }
);
_addonAuthorContainer.hidden = false;
} else {
_addonAuthorContainer.hidden = true;
}
_addonIconElement.setAttribute("src", this.iconURL);
_reasonsPanel.addonType = this.addonType;
_reasonsPanel.update();
_submitPanel.addonType = this.addonType;
_submitPanel.reason = this.reason;
_submitPanel.extensionSupportURL = this.supportURL;
_submitPanel.update();
this.focus();
dispatchCustomEvent(this, "abuse-report:updated", {
addonId,
panel: "reasons",
});
}
setAbuseReport(abuseReport) {
this._report = abuseReport;
// Clear the textarea from any previously entered content.
this._submitPanel.clear();
if (abuseReport) {
this.update();
this.hidden = false;
} else {
this.hidden = true;
}
}
focus() {
if (!this.isConnected || !this.addon) {
return;
}
if (this._reasonsPanel.hidden) {
const { _textarea } = this;
_textarea.focus();
_textarea.select();
} else {
const { _radioCheckedReason } = this;
if (_radioCheckedReason) {
_radioCheckedReason.focus();
}
}
}
cancel() {
if (!this.isConnected || !this.addon) {
return;
}
this._report = null;
dispatchCustomEvent(this, "abuse-report:cancel");
}
submit() {
if (!this.isConnected || !this.addon) {
return;
}
this._report.setMessage(this.message);
this._report.setReason(this.reason);
dispatchCustomEvent(this, "abuse-report:submit", {
addonId: this.addonId,
report: this._report,
});
}
switchToSubmitMode() {
if (!this.isConnected || !this.addon) {
return;
}
this._submitPanel.reason = this.reason;
this._submitPanel.update();
this._reasonsPanel.hidden = true;
this._reasonsPanelButtons.hidden = true;
this._submitPanel.hidden = false;
this._submitPanelButtons.hidden = false;
// Adjust the focused element when switching to the submit panel.
this.focus();
dispatchCustomEvent(this, "abuse-report:updated", {
addonId: this.addonId,
panel: "submit",
});
}
switchToListMode() {
if (!this.isConnected || !this.addon) {
return;
}
this._submitPanel.hidden = true;
this._submitPanelButtons.hidden = true;
this._reasonsPanel.hidden = false;
this._reasonsPanelButtons.hidden = false;
// Adjust the focused element when switching back to the list of reasons.
this.focus();
dispatchCustomEvent(this, "abuse-report:updated", {
addonId: this.addonId,
panel: "reasons",
});
}
get addon() {
return this._report?.addon;
}
get addonId() {
return this.addon?.id;
}
get addonName() {
return this.addon?.name;
}
get addonType() {
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
// implementation is also removed.
if (this.addon?.type === "sitepermission-deprecated") {
return "sitepermission";
}
return this.addon?.type;
}
get addonCreator() {
return this.addon?.creator;
}
get homepageURL() {
return this.addon?.homepageURL || this.authorURL || "";
}
get authorName() {
// The author name may be missing on some of the test extensions
// (or for temporarily installed add-ons).
return this.addonCreator?.name || "";
}
get authorURL() {
return this.addonCreator?.url || "";
}
get iconURL() {
if (this.addonType === "sitepermission") {
return "chrome://mozapps/skin/extensions/category-sitepermission.svg";
}
return (
this.addon?.iconURL ||
// Some extensions (e.g. static theme addons) may not have an icon,
// and so we fallback to use the generic extension icon.
"chrome://mozapps/skin/extensions/extensionGeneric.svg"
);
}
get supportURL() {
let url = this.addon?.supportURL || this.homepageURL || "";
if (!url && this.addonType === "sitepermission" && this.addon?.siteOrigin) {
return this.addon.siteOrigin;
}
return url;
}
get message() {
return this._form.elements.message.value;
}
get reason() {
return this._form.elements.reason.value;
}
get modalTemplate() {
return document.getElementById("tmpl-modal");
}
get template() {
return document.getElementById("tmpl-abuse-report");
}
}
customElements.define("abuse-report-reason-listitem", AbuseReasonListItem, {
extends: "li",
});
customElements.define(
"abuse-report-reason-suggestions",
AbuseReasonSuggestions
);
customElements.define("abuse-report-reasons-panel", AbuseReasonsPanel);
customElements.define("abuse-report-submit-panel", AbuseSubmitPanel);
customElements.define("addon-abuse-report", AbuseReport);
// The panel has been opened in a new dialog window.
if (IS_DIALOG_WINDOW) {
// CSS customizations when panel is in its own window
// (vs. being an about:addons subframe).
document.documentElement.className = "dialog-window";
const { report, deferredReport, deferredReportPanel } =
window.arguments[0].wrappedJSObject;
window.addEventListener(
"unload",
() => {
// If the window has been closed resolve the deferredReport
// promise and reject the deferredReportPanel one, in case
// they haven't been resolved yet.
deferredReport.resolve({ userCancelled: true });
deferredReportPanel.reject(new Error("report dialog closed"));
},
{ once: true }
);
document.l10n.setAttributes(
document.querySelector("head > title"),
"abuse-report-dialog-title",
{
"addon-name": report.addon.name,
}
);
const el = document.querySelector("addon-abuse-report");
el.addEventListener("abuse-report:submit", () => {
deferredReport.resolve({
userCancelled: false,
report,
});
});
el.addEventListener(
"abuse-report:cancel",
() => {
// Resolve the report panel deferred (in case the report
// has been cancelled automatically before it has been fully
// rendered, e.g. in case of non-supported addon types).
deferredReportPanel.resolve(el);
// Resolve the deferred report as cancelled.
deferredReport.resolve({ userCancelled: true });
},
{ once: true }
);
// Adjust window size (if needed) once the fluent strings have been
// added to the document and the document has been flushed.
el.addEventListener(
"abuse-report:updated",
async () => {
const form = document.querySelector("form");
await document.l10n.translateFragment(form);
const { scrollWidth, scrollHeight } = await window.promiseDocumentFlushed(
() => form
);
// Resolve promiseReportPanel once the panel completed the initial render
// (used in tests).
deferredReportPanel.resolve(el);
if (
window.innerWidth !== scrollWidth ||
window.innerHeight !== scrollHeight
) {
const width = window.outerWidth - window.innerWidth + scrollWidth;
const height = window.outerHeight - window.innerHeight + scrollHeight;
window.resizeTo(width, height);
}
},
{ once: true }
);
el.setAbuseReport(report);
}

Просмотреть файл

@ -4,188 +4,22 @@
/* eslint max-len: ["error", 80] */
/* import-globals-from aboutaddonsCommon.js */
/* exported openAbuseReport */
/* exported AbuseReporter, openAbuseReport */
/* global windowRoot */
/**
* This script is part of the HTML about:addons page and it provides some
* helpers used for the Abuse Reporting submission (and related message bars).
* helpers used for abuse reports.
*/
const { AbuseReporter } = ChromeUtils.importESModule(
"resource://gre/modules/AbuseReporter.sys.mjs"
);
// Message Bars definitions.
const ABUSE_REPORT_MESSAGE_BARS = {
// Idle message-bar (used while the submission is still ongoing).
submitting: {
actions: ["cancel"],
l10n: {
id: "abuse-report-messagebar-submitting2",
actionIds: {
cancel: "abuse-report-messagebar-action-cancel",
},
},
},
// Submitted report message-bar.
submitted: {
actions: ["remove", "keep"],
dismissable: true,
l10n: {
id: "abuse-report-messagebar-submitted2",
actionIdsPerAddonType: {
extension: {
remove: "abuse-report-messagebar-action-remove-extension",
keep: "abuse-report-messagebar-action-keep-extension",
},
sitepermission: {
remove: "abuse-report-messagebar-action-remove-sitepermission",
keep: "abuse-report-messagebar-action-keep-sitepermission",
},
theme: {
remove: "abuse-report-messagebar-action-remove-theme",
keep: "abuse-report-messagebar-action-keep-theme",
},
},
},
},
// Submitted report message-bar (with no remove actions).
"submitted-no-remove-action": {
dismissable: true,
l10n: { id: "abuse-report-messagebar-submitted-noremove2" },
},
// Submitted report and remove addon message-bar.
"submitted-and-removed": {
dismissable: true,
l10n: {
idsPerAddonType: {
extension: "abuse-report-messagebar-removed-extension2",
sitepermission: "abuse-report-messagebar-removed-sitepermission2",
theme: "abuse-report-messagebar-removed-theme2",
},
},
},
// The "aborted report" message bar is rendered as a generic informative one,
// because aborting a report is triggered by a user choice.
ERROR_ABORTED_SUBMIT: {
type: "info",
dismissable: true,
l10n: { id: "abuse-report-messagebar-aborted2" },
},
// Errors message bars.
ERROR_ADDON_NOTFOUND: {
type: "error",
dismissable: true,
l10n: { id: "abuse-report-messagebar-error2" },
},
ERROR_CLIENT: {
type: "error",
dismissable: true,
l10n: { id: "abuse-report-messagebar-error2" },
},
ERROR_NETWORK: {
actions: ["retry", "cancel"],
type: "error",
l10n: {
id: "abuse-report-messagebar-error2",
actionIds: {
retry: "abuse-report-messagebar-action-retry",
cancel: "abuse-report-messagebar-action-cancel",
},
},
},
ERROR_RECENT_SUBMIT: {
actions: ["retry", "cancel"],
type: "error",
l10n: {
id: "abuse-report-messagebar-error-recent-submit2",
actionIds: {
retry: "abuse-report-messagebar-action-retry",
cancel: "abuse-report-messagebar-action-cancel",
},
},
},
ERROR_SERVER: {
actions: ["retry", "cancel"],
type: "error",
l10n: {
id: "abuse-report-messagebar-error2",
actionIds: {
retry: "abuse-report-messagebar-action-retry",
cancel: "abuse-report-messagebar-action-cancel",
},
},
},
ERROR_UNKNOWN: {
actions: ["retry", "cancel"],
type: "error",
l10n: {
id: "abuse-report-messagebar-error2",
actionIds: {
retry: "abuse-report-messagebar-action-retry",
cancel: "abuse-report-messagebar-action-cancel",
},
},
},
};
async function openAbuseReport({ addonId }) {
// TODO: `reportEntryPoint` is also passed to this function but we aren't
// using it currently. Maybe we should?
async function openAbuseReport({ addonId, reportEntryPoint }) {
try {
const reportDialog = await AbuseReporter.openDialog(
addonId,
reportEntryPoint,
window.docShell.chromeEventHandler
);
// Warn the user before the about:addons tab while an
// abuse report dialog is still open, and close the
// report dialog if the user choose to close the related
// about:addons tab.
const beforeunloadListener = evt => evt.preventDefault();
const unloadListener = () => reportDialog.close();
const clearUnloadListeners = () => {
window.removeEventListener("beforeunload", beforeunloadListener);
window.removeEventListener("unload", unloadListener);
};
window.addEventListener("beforeunload", beforeunloadListener);
window.addEventListener("unload", unloadListener);
reportDialog.promiseReport
.then(
report => {
if (report) {
submitReport({ report });
}
},
err => {
Cu.reportError(
`Unexpected abuse report panel error: ${err} :: ${err.stack}`
);
reportDialog.close();
}
)
.then(clearUnloadListeners);
} catch (err) {
// Log the detailed error to the browser console.
Cu.reportError(err);
document.dispatchEvent(
new CustomEvent("abuse-report:create-error", {
detail: {
addonId,
addon: err.addon,
errorType: err.errorType,
},
})
);
}
}
// Unlike the openAbuseReport function, technically this method wouldn't need
// to be async, but it is so that both the implementations will be providing
// the same type signatures (returning a promise) to the callers, independently
// from which abuse reporting feature is enabled.
async function openAbuseReportAMOForm({ addonId }) {
const amoUrl = AbuseReporter.getAMOFormURL({ addonId });
windowRoot.ownerGlobal.openTrustedLinkIn(amoUrl, "tab", {
// Make sure the newly open tab is going to be focused, independently
@ -194,183 +28,4 @@ async function openAbuseReportAMOForm({ addonId }) {
});
}
window.openAbuseReport = AbuseReporter.amoFormEnabled
? openAbuseReportAMOForm
: openAbuseReport;
// Helper function used to create abuse report message bars in the
// HTML about:addons page.
function createReportMessageBar(
definitionId,
{ addonId, addonName, addonType },
{ onclose, onaction } = {}
) {
const barInfo = ABUSE_REPORT_MESSAGE_BARS[definitionId];
if (!barInfo) {
throw new Error(`message-bar definition not found: ${definitionId}`);
}
const { dismissable, actions, type, l10n } = barInfo;
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
// implementation is also removed.
const mappingAddonType =
addonType === "sitepermission-deprecated" ? "sitepermission" : addonType;
const getMessageL10n = () => {
return l10n.idsPerAddonType
? l10n.idsPerAddonType[mappingAddonType]
: l10n.id;
};
const getActionL10n = action => {
return l10n.actionIdsPerAddonType
? l10n.actionIdsPerAddonType[mappingAddonType][action]
: l10n.actionIds[action];
};
const messagebar = document.createElement("moz-message-bar");
document.l10n.setAttributes(messagebar, getMessageL10n(), {
"addon-name": addonName || addonId,
});
messagebar.setAttribute("data-l10n-attrs", "message");
actions?.forEach(action => {
const buttonEl = document.createElement("button");
buttonEl.addEventListener("click", () => onaction && onaction(action));
document.l10n.setAttributes(buttonEl, getActionL10n(action));
buttonEl.setAttribute("slot", "actions");
messagebar.appendChild(buttonEl);
});
messagebar.setAttribute("type", type || "info");
messagebar.dismissable = dismissable;
messagebar.addEventListener("message-bar:close", onclose, { once: true });
document.getElementById("abuse-reports-messages").append(messagebar);
document.dispatchEvent(
new CustomEvent("abuse-report:new-message-bar", {
detail: { definitionId, messagebar },
})
);
return messagebar;
}
async function submitReport({ report }) {
const { addon } = report;
const addonId = addon.id;
const addonName = addon.name;
const addonType = addon.type;
// Ensure that the tab that originated the report dialog is selected
// when the user is submitting the report.
const { gBrowser } = window.windowRoot.ownerGlobal;
if (gBrowser && gBrowser.getTabForBrowser) {
let tab = gBrowser.getTabForBrowser(window.docShell.chromeEventHandler);
gBrowser.selectedTab = tab;
}
// Create a message bar while we are still submitting the report.
const mbSubmitting = createReportMessageBar(
"submitting",
{ addonId, addonName, addonType },
{
onaction: action => {
if (action === "cancel") {
report.abort();
mbSubmitting.remove();
}
},
}
);
try {
await report.submit();
mbSubmitting.remove();
// Create a submitted message bar when the submission has been
// successful.
let barId;
if (
!(addon.permissions & AddonManager.PERM_CAN_UNINSTALL) &&
!isPending(addon, "uninstall")
) {
// Do not offer remove action if the addon can't be uninstalled.
barId = "submitted-no-remove-action";
} else if (report.reportEntryPoint === "uninstall") {
// With reportEntryPoint "uninstall" a specific message bar
// is going to be used.
barId = "submitted-and-removed";
} else {
// All the other reportEntryPoint ("menu" and "toolbar_context_menu")
// use the same kind of message bar.
barId = "submitted";
}
const mbInfo = createReportMessageBar(
barId,
{
addonId,
addonName,
addonType,
},
{
onaction: action => {
mbInfo.remove();
// action "keep" doesn't require any further action,
// just handle "remove".
if (action === "remove") {
report.addon.uninstall(true);
}
},
}
);
} catch (err) {
// Log the complete error in the console.
console.error("Error submitting abuse report for", addonId, err);
mbSubmitting.remove();
// The report has a submission error, create a error message bar which
// may optionally allow the user to retry to submit the same report.
const barId =
err.errorType in ABUSE_REPORT_MESSAGE_BARS
? err.errorType
: "ERROR_UNKNOWN";
const mbError = createReportMessageBar(
barId,
{
addonId,
addonName,
addonType,
},
{
onaction: action => {
mbError.remove();
switch (action) {
case "retry":
submitReport({ report });
break;
case "cancel":
report.abort();
break;
}
},
}
);
}
}
document.addEventListener("abuse-report:submit", ({ detail }) => {
submitReport(detail);
});
document.addEventListener("abuse-report:create-error", ({ detail }) => {
const { addonId, addon, errorType } = detail;
const barId =
errorType in ABUSE_REPORT_MESSAGE_BARS ? errorType : "ERROR_UNKNOWN";
createReportMessageBar(barId, {
addonId,
addonName: addon && addon.name,
addonType: addon && addon.type,
});
});
window.openAbuseReport = openAbuseReport;

Просмотреть файл

@ -11,9 +11,6 @@ toolkit.jar:
content/mozapps/extensions/aboutaddonsCommon.js (content/aboutaddonsCommon.js)
content/mozapps/extensions/aboutaddons.css (content/aboutaddons.css)
content/mozapps/extensions/abuse-reports.js (content/abuse-reports.js)
content/mozapps/extensions/abuse-report-frame.html (content/abuse-report-frame.html)
content/mozapps/extensions/abuse-report-panel.css (content/abuse-report-panel.css)
content/mozapps/extensions/abuse-report-panel.js (content/abuse-report-panel.js)
content/mozapps/extensions/drag-drop-addon-installer.js (content/drag-drop-addon-installer.js)
content/mozapps/extensions/shortcuts.css (content/shortcuts.css)
content/mozapps/extensions/shortcuts.js (content/shortcuts.js)

Просмотреть файл

@ -208,50 +208,6 @@ addons_manager:
type: quantity
expires: 132
report:
type: event
description: An abuse report submitted by a user for a given extension.
bugs:
- https://bugzilla.mozilla.org/1544927
- https://bugzilla.mozilla.org/1580561
- https://bugzilla.mozilla.org/1590736
- https://bugzilla.mozilla.org/1630596
- https://bugzilla.mozilla.org/1672570
- https://bugzilla.mozilla.org/1714251
- https://bugzilla.mozilla.org/1749878
- https://bugzilla.mozilla.org/1780746
- https://bugzilla.mozilla.org/1781974
- https://bugzilla.mozilla.org/1817100
- https://bugzilla.mozilla.org/1820153
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1820153#c3
notification_emails:
- addons-dev-internal@mozilla.com
extra_keys:
addon_id:
description: Id of the addon being reported.
type: string
addon_type:
description: |
The type of the add-on being reported (missing on
ERROR_ADDON_NOT_FOUND, ERROR_AMODETAILS_NOTFOUND
and ERROR_AMODETAILS_FAILURE).
type: string
entry_point:
description: |
Report entry point, one of: amo, menu,
toolbar_context_menu, unified_context_menu, uninstall.
type: string
error_type:
description: |
AbuseReport Error Type (included in case of submission
failures). The error types include ERROR_ABORTED_SUBMIT,
ERROR_ADDON_NOT_FOUND, ERROR_CLIENT, ERROR_NETWORK,
ERROR_UNKNOWN, ERROR_RECENT_SUBMIT, ERROR_SERVER,
ERROR_AMODETAILS_NOTFOUND, ERROR_AMODETAILS_FAILURE.
type: string
expires: 132
report_suspicious_site:
type: event
description: |

Просмотреть файл

@ -55,6 +55,7 @@ prefs = [
["browser_addon_list_reordering.js"]
["browser_amo_abuse_report.js"]
support-files = ["head_abuse_report.js"]
["browser_bug572561.js"]
@ -77,12 +78,6 @@ skip-if = ["true"] # Bug 1626824
["browser_history_navigation.js"]
https_first_disabled = true
["browser_html_abuse_report.js"]
support-files = ["head_abuse_report.js"]
["browser_html_abuse_report_dialog.js"]
support-files = ["head_abuse_report.js"]
["browser_html_detail_permissions.js"]
["browser_html_detail_view.js"]
@ -100,8 +95,6 @@ support-files = ["head_disco.js"]
["browser_html_list_view_recommendations.js"]
skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results for .popup-notification-primary-button.primary.footer-button
["browser_html_message_bar.js"]
["browser_html_options_ui.js"]
["browser_html_options_ui_dark_theme.js"]
@ -171,9 +164,6 @@ https_first_disabled = true
["browser_webapi.js"]
["browser_webapi_abuse_report.js"]
support-files = ["head_abuse_report.js"]
["browser_webapi_access.js"]
https_first_disabled = true

Просмотреть файл

@ -15,26 +15,7 @@ add_setup(async () => {
],
});
// Explicitly flip the amoFormEnabled pref on builds where the pref is
// expected to not be set to true by default.
if (AppConstants.MOZ_APP_NAME != "firefox") {
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amoFormEnabled", true]],
});
}
const { AbuseReporter } = ChromeUtils.importESModule(
"resource://gre/modules/AbuseReporter.sys.mjs"
);
Assert.equal(
AbuseReporter.amoFormEnabled,
true,
"Expect AMO abuse report form to be enabled"
);
// Setting up MockProvider to mock various addon types
// as installed.
// Setting up MockProvider to mock various addon types as installed.
await AbuseReportTestUtils.setup();
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,185 +0,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/. */
/* eslint max-len: ["error", 80] */
loadTestSubscript("head_abuse_report.js");
add_setup(async function () {
// Make sure the integrated abuse report panel is the one enabled
// while this test file runs (instead of the AMO hosted form).
// NOTE: behaviors expected when amoFormEnabled is true are tested
// in the separate browser_amo_abuse_report.js test file.
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amoFormEnabled", false]],
});
await AbuseReportTestUtils.setup();
});
/**
* Test tasks specific to the abuse report opened in its own dialog window.
*/
add_task(async function test_close_icon_button_hidden_when_dialog() {
const addonId = "addon-to-report@mochi.test";
const extension = await installTestExtension(addonId);
const reportDialog = await AbuseReporter.openDialog(
addonId,
"menu",
gBrowser.selectedBrowser
);
await AbuseReportTestUtils.promiseReportDialogRendered();
const panelEl = await reportDialog.promiseReportPanel;
let promiseClosedWindow = waitClosedWindow();
EventUtils.synthesizeKey("VK_RETURN", {}, panelEl.ownerGlobal);
AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
await promiseClosedWindow;
ok(
await reportDialog.promiseReport,
"expect the report to not be cancelled by pressing enter"
);
await extension.unload();
});
add_task(async function test_report_triggered_when_report_dialog_is_open() {
const addonId = "addon-to-report@mochi.test";
const extension = await installTestExtension(addonId);
const reportDialog = await AbuseReporter.openDialog(
addonId,
"menu",
gBrowser.selectedBrowser
);
await AbuseReportTestUtils.promiseReportDialogRendered();
let promiseClosedWindow = waitClosedWindow();
const reportDialog2 = await AbuseReporter.openDialog(
addonId,
"menu",
gBrowser.selectedBrowser
);
await promiseClosedWindow;
// Trigger the report submit and check that the second report is
// resolved as expected.
await AbuseReportTestUtils.promiseReportDialogRendered();
ok(
!reportDialog.window || reportDialog.window.closed,
"expect the first dialog to be closed"
);
ok(!!reportDialog2.window, "expect the second dialog to be open");
is(
reportDialog2.window,
AbuseReportTestUtils.getReportDialog(),
"Got a report dialog as expected"
);
AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message");
// promiseReport is resolved to undefined if the report has been
// cancelled, otherwise it is resolved to a report object.
ok(
!(await reportDialog.promiseReport),
"expect the first report to be cancelled"
);
ok(
!!(await reportDialog2.promiseReport),
"expect the second report to be resolved"
);
await extension.unload();
});
add_task(async function test_report_dialog_window_closed_by_user() {
const addonId = "addon-to-report@mochi.test";
const extension = await installTestExtension(addonId);
const reportDialog = await AbuseReporter.openDialog(
addonId,
"menu",
gBrowser.selectedBrowser
);
await AbuseReportTestUtils.promiseReportDialogRendered();
let promiseClosedWindow = waitClosedWindow();
reportDialog.close();
await promiseClosedWindow;
ok(
!(await reportDialog.promiseReport),
"expect promiseReport to be resolved as user cancelled"
);
await extension.unload();
});
add_task(async function test_amo_details_for_not_installed_addon() {
const addonId = "not-installed-addon@mochi.test";
const fakeAMODetails = {
name: "fake name",
current_version: { version: "1.0" },
type: "extension",
icon_url: "http://test.addons.org/asserts/fake-icon-url.png",
homepage: "http://fake.url/homepage",
support_url: "http://fake.url/support",
authors: [
{ name: "author1", url: "http://fake.url/author1" },
{ name: "author2", url: "http://fake.url/author2" },
],
is_recommended: true,
};
AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
registerCleanupFunction(() =>
AbuseReportTestUtils.amoAddonDetailsMap.clear()
);
const reportDialog = await AbuseReporter.openDialog(
addonId,
"menu",
gBrowser.selectedBrowser
);
const reportEl = await reportDialog.promiseReportPanel;
// Assert that the panel has been able to retrieve from AMO
// all the addon details needed to render the panel correctly.
is(reportEl.addonId, addonId, "Got the expected addonId");
is(reportEl.addonName, fakeAMODetails.name, "Got the expected addon name");
is(reportEl.addonType, fakeAMODetails.type, "Got the expected addon type");
is(
reportEl.authorName,
fakeAMODetails.authors[0].name,
"Got the first author name as expected"
);
is(
reportEl.authorURL,
fakeAMODetails.authors[0].url,
"Got the first author url as expected"
);
is(reportEl.iconURL, fakeAMODetails.icon_url, "Got the expected icon url");
is(
reportEl.supportURL,
fakeAMODetails.support_url,
"Got the expected support url"
);
is(
reportEl.homepageURL,
fakeAMODetails.homepage,
"Got the expected homepage url"
);
reportDialog.close();
});

Просмотреть файл

@ -1,185 +0,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/. */
/* eslint max-len: ["error", 80] */
let htmlAboutAddonsWindow;
const HTML_NS = "http://www.w3.org/1999/xhtml";
function clickElement(el) {
el.dispatchEvent(new CustomEvent("click"));
}
function createMessageBar(messageBarStack, { attrs, children, onclose } = {}) {
const win = messageBarStack.ownerGlobal;
const messageBar = win.document.createElementNS(HTML_NS, "message-bar");
if (attrs) {
for (const [k, v] of Object.entries(attrs)) {
messageBar.setAttribute(k, v);
}
}
if (children) {
if (Array.isArray(children)) {
messageBar.append(...children);
} else {
messageBar.append(children);
}
}
messageBar.addEventListener("message-bar:close", onclose, { once: true });
messageBarStack.append(messageBar);
return messageBar;
}
add_setup(async function () {
htmlAboutAddonsWindow = await loadInitialView("extension");
registerCleanupFunction(() => closeView(htmlAboutAddonsWindow));
});
add_task(async function test_message_bar_stack() {
const win = htmlAboutAddonsWindow;
let messageBarStack = win.document.getElementById("abuse-reports-messages");
ok(messageBarStack, "Got a message-bar-stack in HTML about:addons page");
is(
messageBarStack.maxMessageBarCount,
3,
"Got the expected max-message-bar-count property"
);
is(
messageBarStack.childElementCount,
0,
"message-bar-stack is initially empty"
);
});
add_task(async function test_create_message_bar_create_and_onclose() {
const win = htmlAboutAddonsWindow;
const messageBarStack = win.document.getElementById("abuse-reports-messages");
let messageEl = win.document.createElementNS(HTML_NS, "span");
messageEl.textContent = "A message bar text";
let buttonEl = win.document.createElementNS(HTML_NS, "button");
buttonEl.textContent = "An action button";
let messageBar;
let onceMessageBarClosed = new Promise(resolve => {
messageBar = createMessageBar(messageBarStack, {
children: [messageEl, buttonEl],
onclose: resolve,
});
});
is(
messageBarStack.childElementCount,
1,
"message-bar-stack has a child element"
);
is(
messageBarStack.firstElementChild,
messageBar,
"newly created message-bar added as message-bar-stack child element"
);
const slot = messageBar.shadowRoot.querySelector("slot");
is(
slot.assignedNodes()[0],
messageEl,
"Got the expected span element assigned to the message-bar slot"
);
is(
slot.assignedNodes()[1],
buttonEl,
"Got the expected button element assigned to the message-bar slot"
);
let dismissed = BrowserTestUtils.waitForEvent(
messageBar,
"message-bar:user-dismissed"
);
info("Click the close icon on the newly created message-bar");
clickElement(messageBar.closeButton);
await dismissed;
info("Expect the onclose function to be called");
await onceMessageBarClosed;
is(
messageBarStack.childElementCount,
0,
"message-bar-stack has no child elements"
);
});
add_task(async function test_max_message_bar_count() {
const win = htmlAboutAddonsWindow;
const messageBarStack = win.document.getElementById("abuse-reports-messages");
info("Create a new message-bar");
let messageElement = document.createElementNS(HTML_NS, "span");
messageElement = "message bar label";
let onceMessageBarClosed = new Promise(resolve => {
createMessageBar(messageBarStack, {
children: messageElement,
onclose: resolve,
});
});
is(
messageBarStack.childElementCount,
1,
"message-bar-stack has the expected number of children"
);
info("Create 3 more message bars");
const allBarsPromises = [];
for (let i = 2; i <= 4; i++) {
allBarsPromises.push(
new Promise(resolve => {
createMessageBar(messageBarStack, {
attrs: { dismissable: "" },
children: [messageElement, i],
onclose: resolve,
});
})
);
}
info("Expect first message-bar to closed automatically");
await onceMessageBarClosed;
is(
messageBarStack.childElementCount,
3,
"message-bar-stack has the expected number of children"
);
info("Click on close icon for the second message-bar");
clickElement(messageBarStack.firstElementChild.closeButton);
info("Expect the second message-bar to be closed");
await allBarsPromises[0];
is(
messageBarStack.childElementCount,
2,
"message-bar-stack has the expected number of children"
);
info("Clear the entire message-bar-stack content");
messageBarStack.textContent = "";
info("Expect all the created message-bar to be closed automatically");
await Promise.all(allBarsPromises);
is(
messageBarStack.childElementCount,
0,
"message-bar-stack has no child elements"
);
});

Просмотреть файл

@ -1,375 +0,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/. */
/* eslint max-len: ["error", 80] */
loadTestSubscript("head_abuse_report.js");
const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
const TELEMETRY_EVENTS_FILTERS = {
category: "addonsManager",
method: "report",
};
const REPORT_PROP_NAMES = [
"addon",
"addon_signature",
"reason",
"message",
"report_entry_point",
];
function getObjectProps(obj, propNames) {
const res = {};
for (const k of propNames) {
res[k] = obj[k];
}
return res;
}
async function assertSubmittedReport(expectedReportProps) {
let reportSubmitted;
const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled(
({ data, request, response }) => {
reportSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
}
);
let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
let promiseWinClosed = waitClosedWindow();
let promisePanelUpdated = AbuseReportTestUtils.promiseReportUpdated(
panelEl,
"submit"
);
panelEl._form.elements.reason.value = expectedReportProps.reason;
AbuseReportTestUtils.clickPanelButton(panelEl._btnNext);
await promisePanelUpdated;
panelEl._form.elements.message.value = expectedReportProps.message;
// Reset the timestamp of the last report between tests.
AbuseReporter._lastReportTimestamp = null;
AbuseReportTestUtils.clickPanelButton(panelEl._btnSubmit);
await Promise.all([onReportSubmitted, promiseWinClosed]);
ok(!panelEl.ownerGlobal, "Report dialog window is closed");
Assert.deepEqual(
getObjectProps(reportSubmitted, REPORT_PROP_NAMES),
expectedReportProps,
"Got the expected report data submitted"
);
}
add_setup(async function () {
await AbuseReportTestUtils.setup();
await SpecialPowers.pushPrefEnv({
set: [
["extensions.webapi.testing", true],
["extensions.abuseReport.amWebAPI.enabled", true],
// Make sure the integrated abuse report panel is the one enabled
// while this test file runs (instead of the AMO hosted form).
// NOTE: behaviors expected when amoFormEnabled is true are tested
// in the separate browser_amo_abuse_report.js test file.
["extensions.abuseReport.amoFormEnabled", false],
],
});
});
add_task(async function test_report_installed_addon_cancelled() {
Services.telemetry.clearEvents();
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
const extension = await installTestExtension(ADDON_ID);
let reportEnabled = await SpecialPowers.spawn(browser, [], () => {
return content.navigator.mozAddonManager.abuseReportPanelEnabled;
});
is(reportEnabled, true, "Expect abuseReportPanelEnabled to be true");
info("Test reportAbuse result on user cancelled report");
let promiseNewWindow = waitForNewWindow();
let promiseWebAPIResult = SpecialPowers.spawn(
browser,
[ADDON_ID],
addonId => content.navigator.mozAddonManager.reportAbuse(addonId)
);
let win = await promiseNewWindow;
is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
let promiseWinClosed = waitClosedWindow();
AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel);
let reportResult = await promiseWebAPIResult;
is(
reportResult,
false,
"Expect reportAbuse to resolve to false on user cancelled report"
);
await promiseWinClosed;
ok(!panelEl.ownerGlobal, "Report dialog window is closed");
await extension.unload();
});
// Expect no telemetry events collected for user cancelled reports.
TelemetryTestUtils.assertEvents([], TELEMETRY_EVENTS_FILTERS);
});
add_task(async function test_report_installed_addon_submitted() {
Services.telemetry.clearEvents();
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
const extension = await installTestExtension(ADDON_ID);
let promiseNewWindow = waitForNewWindow();
let promiseWebAPIResult = SpecialPowers.spawn(browser, [ADDON_ID], id =>
content.navigator.mozAddonManager.reportAbuse(id)
);
let win = await promiseNewWindow;
is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
await assertSubmittedReport({
addon: ADDON_ID,
addon_signature: "missing",
message: "fake report message",
reason: "unwanted",
report_entry_point: "amo",
});
let reportResult = await promiseWebAPIResult;
is(
reportResult,
true,
"Expect reportAbuse to resolve to false on user cancelled report"
);
await extension.unload();
});
TelemetryTestUtils.assertEvents(
[
{
object: "amo",
value: ADDON_ID,
extra: { addon_type: "extension" },
},
],
TELEMETRY_EVENTS_FILTERS
);
});
add_task(async function test_report_unknown_not_installed_addon() {
const addonId = "unknown-addon@mochi.test";
Services.telemetry.clearEvents();
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id =>
content.navigator.mozAddonManager.reportAbuse(id).catch(err => {
return { name: err.name, message: err.message };
})
);
await Assert.deepEqual(
await promiseWebAPIResult,
{ name: "Error", message: "Error creating abuse report" },
"Got the expected rejected error on reporting unknown addon"
);
ok(!AbuseReportTestUtils.getReportDialog(), "No report dialog is open");
});
TelemetryTestUtils.assertEvents(
[
{
object: "amo",
value: addonId,
extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" },
},
{
object: "amo",
value: addonId,
extra: { error_type: "ERROR_ADDON_NOTFOUND" },
},
],
TELEMETRY_EVENTS_FILTERS
);
});
add_task(async function test_report_not_installed_addon() {
const addonId = "not-installed-addon@mochi.test";
Services.telemetry.clearEvents();
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
const fakeAMODetails = {
name: "fake name",
current_version: { version: "1.0" },
type: "extension",
icon_url: "http://test.addons.org/asserts/fake-icon-url.png",
homepage: "http://fake.url/homepage",
authors: [{ name: "author1", url: "http://fake.url/author1" }],
is_recommended: false,
};
AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
registerCleanupFunction(() =>
AbuseReportTestUtils.amoAddonDetailsMap.clear()
);
let promiseNewWindow = waitForNewWindow();
let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id =>
content.navigator.mozAddonManager.reportAbuse(id)
);
let win = await promiseNewWindow;
is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
await assertSubmittedReport({
addon: addonId,
addon_signature: "unknown",
message: "fake report message",
reason: "other",
report_entry_point: "amo",
});
let reportResult = await promiseWebAPIResult;
is(
reportResult,
true,
"Expect reportAbuse to resolve to true on submitted report"
);
});
TelemetryTestUtils.assertEvents(
[
{
object: "amo",
value: addonId,
extra: { addon_type: "extension" },
},
],
TELEMETRY_EVENTS_FILTERS
);
});
add_task(async function test_amo_report_on_report_already_inprogress() {
const extension = await installTestExtension(ADDON_ID);
const reportDialog = await AbuseReporter.openDialog(
ADDON_ID,
"menu",
gBrowser.selectedBrowser
);
await AbuseReportTestUtils.promiseReportDialogRendered();
ok(reportDialog.window, "Got an open report dialog");
let promiseWinClosed = waitClosedWindow();
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
const promiseAMOResult = SpecialPowers.spawn(browser, [ADDON_ID], id =>
content.navigator.mozAddonManager.reportAbuse(id)
);
await promiseWinClosed;
ok(reportDialog.window.closed, "previous report dialog should be closed");
is(
await reportDialog.promiseAMOResult,
undefined,
"old report cancelled after AMO called mozAddonManager.reportAbuse"
);
const panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
const { report } = AbuseReportTestUtils.getReportDialogParams();
Assert.deepEqual(
{
reportEntryPoint: report.reportEntryPoint,
addonId: report.addon.id,
},
{
reportEntryPoint: "amo",
addonId: ADDON_ID,
},
"Got the expected report from the opened report dialog"
);
promiseWinClosed = waitClosedWindow();
AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel);
await promiseWinClosed;
is(
await promiseAMOResult,
false,
"AMO report request resolved to false on cancel button clicked"
);
});
await extension.unload();
});
add_task(async function test_reject_on_unsupported_addon_types() {
const addonId = "not-supported-addon-type@mochi.test";
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
const fakeAMODetails = {
name: "fake name",
current_version: { version: "1.0" },
type: "fake-unsupported-addon-type",
};
AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
registerCleanupFunction(() =>
AbuseReportTestUtils.amoAddonDetailsMap.clear()
);
let webAPIResult = await SpecialPowers.spawn(browser, [addonId], id =>
content.navigator.mozAddonManager.reportAbuse(id).then(
res => ({ gotRejection: false, result: res }),
err => ({ gotRejection: true, message: err.message })
)
);
Assert.deepEqual(
webAPIResult,
{ gotRejection: true, message: "Error creating abuse report" },
"Got the expected rejection from mozAddonManager.reportAbuse"
);
});
});
add_task(async function test_report_on_disabled_webapi() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.abuseReport.amWebAPI.enabled", false]],
});
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
let reportEnabled = await SpecialPowers.spawn(browser, [], () => {
return content.navigator.mozAddonManager.abuseReportPanelEnabled;
});
is(reportEnabled, false, "Expect abuseReportPanelEnabled to be false");
info("Test reportAbuse result on report webAPI disabled");
let promiseWebAPIResult = SpecialPowers.spawn(
browser,
["an-addon@mochi.test"],
addonId =>
content.navigator.mozAddonManager.reportAbuse(addonId).catch(err => {
return { name: err.name, message: err.message };
})
);
Assert.deepEqual(
await promiseWebAPIResult,
{ name: "Error", message: "amWebAPI reportAbuse not supported" },
"Got the expected rejected error"
);
});
await SpecialPowers.popPrefEnv();
});

Просмотреть файл

@ -3,35 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint max-len: ["error", 80] */
/* exported installTestExtension, addCommonAbuseReportTestTasks,
* createPromptConfirmEx, DEFAULT_BUILTIN_THEME_ID,
* gManagerWindow, handleSubmitRequest, makeWidgetId,
* waitForNewWindow, waitClosedWindow, AbuseReporter,
* AbuseReporterTestUtils, AddonTestUtils
/* exported AbuseReportTestUtils, openAboutAddons, closeAboutAddons,
* gManagerWindow
*/
/* global MockProvider, loadInitialView, closeView */
const { AbuseReporter } = ChromeUtils.importESModule(
"resource://gre/modules/AbuseReporter.sys.mjs"
);
const { AddonTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/AddonTestUtils.sys.mjs"
);
const { ExtensionCommon } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionCommon.sys.mjs"
);
const { makeWidgetId } = ExtensionCommon;
const ADDON_ID = "test-extension-to-report@mochi.test";
const REPORT_ENTRY_POINT = "menu";
const BASE_TEST_MANIFEST = {
name: "Fake extension to report",
author: "Fake author",
homepage_url: "https://fake.extension.url/",
};
const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org";
const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test";
const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test";
const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test";
@ -54,110 +35,6 @@ async function closeAboutAddons() {
}
}
function waitForNewWindow() {
return new Promise(resolve => {
let listener = win => {
Services.obs.removeObserver(listener, "toplevel-window-ready");
resolve(win);
};
Services.obs.addObserver(listener, "toplevel-window-ready");
});
}
function waitClosedWindow(win) {
return new Promise(resolve => {
function onWindowClosed() {
if (win && !win.closed) {
// If a specific window reference has been passed, then check
// that the window is closed before resolving the promise.
return;
}
Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
resolve();
}
Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
});
}
async function installTestExtension(
id = ADDON_ID,
type = "extension",
manifest = {}
) {
let additionalProps = {
icons: {
32: "test-icon.png",
},
};
switch (type) {
case "theme":
additionalProps = {
...additionalProps,
theme: {
colors: {
frame: "#a14040",
tab_background_text: "#fac96e",
},
},
};
break;
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
// implementation is also removed.
case "sitepermission-deprecated":
additionalProps = {
name: "WebMIDI test addon for https://mochi.test",
install_origins: ["https://mochi.test"],
site_permissions: ["midi"],
};
break;
case "extension":
break;
default:
throw new Error(`Unexpected addon type: ${type}`);
}
const extensionOpts = {
manifest: {
...BASE_TEST_MANIFEST,
...additionalProps,
...manifest,
browser_specific_settings: { gecko: { id } },
},
useAddonManager: "temporary",
};
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based
// implementation is also removed.
if (type === "sitepermission-deprecated") {
const xpi = AddonTestUtils.createTempWebExtensionFile(extensionOpts);
const addon = await AddonManager.installTemporaryAddon(xpi);
// The extension object that ExtensionTestUtils.loadExtension returns for
// mochitest is pretty tight to the Extension class, and so for now this
// returns a more minimal `extension` test object which only provides the
// `unload` method.
//
// For the purpose of the abuse reports tests that are using this helper
// this should be already enough.
return {
addon,
unload: () => addon.uninstall(),
};
}
const extension = ExtensionTestUtils.loadExtension(extensionOpts);
await extension.startup();
return extension;
}
function handleSubmitRequest({ request, response }) {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json", false);
response.write("{}");
}
const AbuseReportTestUtils = {
_mockProvider: null,
_mockServer: null,
@ -166,228 +43,14 @@ const AbuseReportTestUtils = {
// Mock addon details API endpoint.
amoAddonDetailsMap: new Map(),
// Setup the test environment by setting the expected prefs and
// initializing MockProvider and the mock AMO server.
// Setup the test environment by setting the expected prefs and initializing
// MockProvider.
async setup() {
// Enable html about:addons and the abuse reporting and
// set the api endpoints url to the mock service.
await SpecialPowers.pushPrefEnv({
set: [
["extensions.abuseReport.enabled", true],
["extensions.abuseReport.url", "http://test.addons.org/api/report/"],
[
"extensions.abuseReport.amoDetailsURL",
"http://test.addons.org/api/addons/addon/",
],
],
set: [["extensions.abuseReport.enabled", true]],
});
this._setupMockProvider();
this._setupMockServer();
},
// Returns the currently open abuse report dialog window (if any).
getReportDialog() {
return Services.ww.getWindowByName("addons-abuse-report-dialog");
},
// Returns the parameters related to the report dialog (if any).
getReportDialogParams() {
const win = this.getReportDialog();
return win && win.arguments[0] && win.arguments[0].wrappedJSObject;
},
// Returns a reference to the addon-abuse-report element from the currently
// open abuse report.
getReportPanel() {
const win = this.getReportDialog();
ok(win, "Got an abuse report dialog open");
return win && win.document.querySelector("addon-abuse-report");
},
// Returns the list of abuse report reasons.
getReasons(abuseReportEl) {
return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS);
},
// Returns the info related to a given abuse report reason.
getReasonInfo(abuseReportEl, reason) {
return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason];
},
async promiseReportOpened({ addonId, reportEntryPoint }) {
let abuseReportEl;
if (!this.getReportDialog()) {
info("Wait for the report dialog window");
const dialog = await waitForNewWindow();
is(dialog, this.getReportDialog(), "Report dialog opened");
}
info("Wait for the abuse report panel render");
abuseReportEl = await AbuseReportTestUtils.promiseReportDialogRendered();
ok(abuseReportEl, "Got an abuse report panel");
is(
abuseReportEl.addon && abuseReportEl.addon.id,
addonId,
"Abuse Report panel rendered for the expected addonId"
);
is(
abuseReportEl._report && abuseReportEl._report.reportEntryPoint,
reportEntryPoint,
"Abuse Report panel rendered for the expected reportEntryPoint"
);
return abuseReportEl;
},
// Return a promise resolved when the currently open report panel
// is closed.
// Also asserts that a specific report panel element has been closed,
// if one has been provided through the optional panel parameter.
async promiseReportClosed(panel) {
const win = panel ? panel.ownerGlobal : this.getReportDialog();
if (!win || win.closed) {
throw Error("Expected report dialog not found or already closed");
}
await waitClosedWindow(win);
// Assert that the panel has been closed (if the caller has passed it).
if (panel) {
ok(!panel.ownerGlobal, "abuse report dialog closed");
}
},
// Returns a promise resolved when the report panel has been rendered
// (rejects is there is no dialog currently open).
async promiseReportDialogRendered() {
const params = this.getReportDialogParams();
if (!params) {
throw new Error("abuse report dialog not found");
}
return params.promiseReportPanel;
},
// Given a `requestHandler` function, an HTTP server handler function
// to use to handle a report submit request received by the mock AMO server),
// returns a promise resolved when the mock AMO server has received and
// handled the report submit request.
async promiseReportSubmitHandled(requestHandler) {
if (typeof requestHandler !== "function") {
throw new Error("requestHandler should be a function");
}
return new Promise((resolve, reject) => {
this._abuseRequestHandlers.unshift({ resolve, reject, requestHandler });
});
},
// Return a promise resolved to the abuse report panel element,
// once its rendering is completed.
// If abuseReportEl is undefined, it looks for the currently opened
// report panel.
async promiseReportRendered(abuseReportEl) {
let el = abuseReportEl;
if (!el) {
const win = this.getReportDialog();
if (!win) {
await waitForNewWindow();
}
el = await this.promiseReportDialogRendered();
ok(el, "Got an abuse report panel");
}
return el._radioCheckedReason
? el
: BrowserTestUtils.waitForEvent(
el,
"abuse-report:updated",
"Wait the abuse report panel to be rendered"
).then(() => el);
},
// A promise resolved when the given abuse report panel element
// has been rendered. If a panel name ("reasons" or "submit") is
// passed as a second parameter, it also asserts that the panel is
// updated to the expected view mode.
async promiseReportUpdated(abuseReportEl, panel) {
const evt = await BrowserTestUtils.waitForEvent(
abuseReportEl,
"abuse-report:updated",
"Wait abuse report panel update"
);
if (panel) {
is(evt.detail.panel, panel, `Got a "${panel}" update event`);
const el = abuseReportEl;
switch (evt.detail.panel) {
case "reasons":
ok(!el._reasonsPanel.hidden, "Reasons panel should be visible");
ok(el._submitPanel.hidden, "Submit panel should be hidden");
break;
case "submit":
ok(el._reasonsPanel.hidden, "Reasons panel should be hidden");
ok(!el._submitPanel.hidden, "Submit panel should be visible");
break;
}
}
},
// Returns a promise resolved once the expected number of abuse report
// message bars have been created.
promiseMessageBars(expectedMessageBarCount) {
return new Promise(resolve => {
const details = [];
function listener(evt) {
details.push(evt.detail);
if (details.length >= expectedMessageBarCount) {
cleanup();
resolve(details);
}
}
function cleanup() {
if (gManagerWindow) {
gManagerWindow.document.removeEventListener(
"abuse-report:new-message-bar",
listener
);
}
}
gManagerWindow.document.addEventListener(
"abuse-report:new-message-bar",
listener
);
});
},
async assertFluentStrings(containerEl) {
// Make sure all localized elements have defined Fluent strings.
let localizedEls = Array.from(
containerEl.querySelectorAll("[data-l10n-id]")
);
if (containerEl.getAttribute("data-l10n-id")) {
localizedEls.push(containerEl);
}
ok(localizedEls.length, "Got localized elements");
for (let el of localizedEls) {
const l10nId = el.getAttribute("data-l10n-id");
const l10nAttrs = el.getAttribute("data-l10n-attrs");
if (!l10nAttrs) {
await TestUtils.waitForCondition(
() => el.textContent !== "",
`Element with Fluent id '${l10nId}' should not be empty`
);
} else {
await TestUtils.waitForCondition(
() => el.message !== "",
`Message attribute of the element with Fluent id '${l10nId}'
should not be empty`
);
}
}
},
// Assert that the report action visibility on the addon card
@ -419,68 +82,6 @@ const AbuseReportTestUtils = {
return this.assertReportActionVisibility(gManagerWindow, extId, true);
},
// Assert that the report panel is hidden (or closed if the report
// panel is opened in its own dialog window).
async assertReportPanelHidden() {
const win = this.getReportDialog();
ok(!win, "Abuse Report dialog should be initially hidden");
},
createMockAddons(mockProviderAddons) {
this._mockProvider.createAddons(mockProviderAddons);
},
async clickPanelButton(buttonEl, { label = undefined } = {}) {
info(`Clicking the '${buttonEl.textContent.trim() || label}' button`);
// NOTE: ideally this should synthesize the mouse event,
// we call the click method to prevent intermittent timeouts
// due to the mouse event not received by the target element.
buttonEl.click();
},
triggerNewReport(addonId, reportEntryPoint) {
gManagerWindow.openAbuseReport({ addonId, reportEntryPoint });
},
triggerSubmit(reason, message) {
const reportEl =
this.getReportDialog().document.querySelector("addon-abuse-report");
reportEl._form.elements.message.value = message;
reportEl._form.elements.reason.value = reason;
reportEl.submit();
},
async openReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) {
// Close the current about:addons window if it has been leaved open from
// a previous test case failure.
if (gManagerWindow) {
await closeAboutAddons();
}
await openAboutAddons();
let promiseReportPanel = waitForNewWindow().then(() =>
this.promiseReportDialogRendered()
);
this.triggerNewReport(addonId, reportEntryPoint);
const panelEl = await promiseReportPanel;
await this.promiseReportRendered(panelEl);
is(panelEl.addonId, addonId, `Got Abuse Report panel for ${addonId}`);
return panelEl;
},
async closeReportPanel(panelEl) {
const onceReportClosed = AbuseReportTestUtils.promiseReportClosed(panelEl);
info("Cancel report and wait the dialog to be closed");
panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel"));
await onceReportClosed;
},
// Internal helper methods.
_setupMockProvider() {
@ -529,87 +130,4 @@ const AbuseReportTestUtils = {
},
]);
},
_setupMockServer() {
if (this._mockServer) {
return;
}
// Init test report api server.
const server = AddonTestUtils.createHttpServer({
hosts: ["test.addons.org"],
});
this._mockServer = server;
server.registerPathHandler("/api/report/", (request, response) => {
const stream = request.bodyInputStream;
const buffer = NetUtil.readInputStream(stream, stream.available());
const data = new TextDecoder().decode(buffer);
const promisedHandler = this._abuseRequestHandlers.pop();
if (promisedHandler) {
const { requestHandler, resolve, reject } = promisedHandler;
try {
requestHandler({ data, request, response });
resolve();
} catch (err) {
ok(false, `Unexpected requestHandler error ${err} ${err.stack}\n`);
reject(err);
}
} else {
ok(false, `Unexpected request: ${request.path} ${data}`);
}
});
server.registerPrefixHandler("/api/addons/addon/", (request, response) => {
const addonId = request.path.split("/").pop();
if (!this.amoAddonDetailsMap.has(addonId)) {
response.setStatusLine(request.httpVersion, 404, "Not Found");
response.write(JSON.stringify({ detail: "Not found." }));
} else {
response.setStatusLine(request.httpVersion, 200, "Success");
response.write(JSON.stringify(this.amoAddonDetailsMap.get(addonId)));
}
});
server.registerPathHandler(
"/assets/fake-icon-url.png",
(request, response) => {
response.setStatusLine(request.httpVersion, 200, "Success");
response.write("");
response.finish();
}
);
},
};
function createPromptConfirmEx({
remove = false,
report = false,
expectCheckboxHidden = false,
} = {}) {
return (...args) => {
const checkboxState = args.pop();
const checkboxMessage = args.pop();
is(
checkboxState && checkboxState.value,
false,
"checkboxState should be initially false"
);
if (expectCheckboxHidden) {
ok(
!checkboxMessage,
"Should not have a checkboxMessage in promptService.confirmEx call"
);
} else {
ok(
checkboxMessage,
"Got a checkboxMessage in promptService.confirmEx call"
);
}
// Report checkbox selected.
checkboxState.value = report;
// Remove accepted.
return remove ? 0 : 1;
};
}

Просмотреть файл

@ -2,88 +2,21 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const { AbuseReporter, AbuseReportError } = ChromeUtils.importESModule(
const { AbuseReporter } = ChromeUtils.importESModule(
"resource://gre/modules/AbuseReporter.sys.mjs"
);
const { ClientID } = ChromeUtils.importESModule(
"resource://gre/modules/ClientID.sys.mjs"
);
const { TelemetryController } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryController.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const APPNAME = "XPCShell";
const APPVERSION = "1";
const ADDON_ID = "test-addon@tests.mozilla.org";
const ADDON_ID2 = "test-addon2@tests.mozilla.org";
const FAKE_INSTALL_INFO = {
source: "fake-Install:Source",
method: "fake:install method",
};
const PREF_REQUIRED_LOCALE = "intl.locale.requested";
const REPORT_OPTIONS = { reportEntryPoint: "menu" };
const TELEMETRY_EVENTS_FILTERS = {
category: "addonsManager",
method: "report",
};
const FAKE_AMO_DETAILS = {
name: {
"en-US": "fake name",
"it-IT": "fake it-IT name",
},
current_version: { version: "1.0" },
type: "extension",
is_recommended: true,
};
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
const server = createHttpServer({ hosts: ["test.addons.org"] });
// Mock abuse report API endpoint.
let apiRequestHandler;
server.registerPathHandler("/api/report/", (request, response) => {
const stream = request.bodyInputStream;
const buffer = NetUtil.readInputStream(stream, stream.available());
const data = new TextDecoder().decode(buffer);
apiRequestHandler({ data, request, response });
});
// Mock addon details API endpoint.
const amoAddonDetailsMap = new Map();
server.registerPrefixHandler("/api/addons/addon/", (request, response) => {
const addonId = request.path.split("/").pop();
if (!amoAddonDetailsMap.has(addonId)) {
response.setStatusLine(request.httpVersion, 404, "Not Found");
response.write(JSON.stringify({ detail: "Not found." }));
} else {
response.setStatusLine(request.httpVersion, 200, "Success");
response.write(JSON.stringify(amoAddonDetailsMap.get(addonId)));
}
});
function getProperties(obj, propNames) {
return propNames.reduce((acc, el) => {
acc[el] = obj[el];
return acc;
}, {});
}
function handleSubmitRequest({ request, response }) {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json", false);
response.write("{}");
}
function clearAbuseReportState() {
// Clear the timestamp of the last submission.
AbuseReporter._lastReportTimestamp = null;
}
async function installTestExtension(overrideOptions = {}) {
const extOptions = {
@ -104,34 +37,6 @@ async function installTestExtension(overrideOptions = {}) {
return { extension, addon };
}
async function assertRejectsAbuseReportError(promise, errorType, errorInfo) {
let error;
await Assert.rejects(
promise,
err => {
// Log the actual error to make investigating test failures easier.
Cu.reportError(err);
error = err;
return err instanceof AbuseReportError;
},
`Got an AbuseReportError`
);
equal(error.errorType, errorType, "Got the expected errorType");
equal(error.errorInfo, errorInfo, "Got the expected errorInfo");
ok(
error.message.includes(errorType),
"errorType should be included in the error message"
);
if (errorInfo) {
ok(
error.message.includes(errorInfo),
"errorInfo should be included in the error message"
);
}
}
async function assertBaseReportData({ reportData, addon }) {
// Report properties related to addon metadata.
equal(reportData.addon, ADDON_ID, "Got expected 'addon'");
@ -191,42 +96,10 @@ async function assertBaseReportData({ reportData, addon }) {
);
}
add_task(async function test_setup() {
Services.prefs.setCharPref(
"extensions.abuseReport.url",
"http://test.addons.org/api/report/"
);
Services.prefs.setCharPref(
"extensions.abuseReport.amoDetailsURL",
"http://test.addons.org/api/addons/addon"
);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
add_setup(async () => {
await promiseStartupManager();
// Telemetry test setup needed to ensure that the builtin events are defined
// and they can be collected and verified.
await TelemetryController.testSetup();
// This is actually only needed on Android, because it does not properly support unified telemetry
// and so, if not enabled explicitly here, it would make these tests to fail when running on a
// non-Nightly build.
const oldCanRecordBase = Services.telemetry.canRecordBase;
Services.telemetry.canRecordBase = true;
registerCleanupFunction(() => {
Services.telemetry.canRecordBase = oldCanRecordBase;
});
// Register a fake it-IT locale (used to test localized AMO details in some
// of the test case defined in this test file).
L10nRegistry.getInstance().registerSources([
L10nFileSource.createMock(
"mock",
"app",
["it-IT", "fr-FR"],
"resource://fake/locales/{locale}",
[]
),
]);
});
add_task(async function test_addon_report_data() {
@ -260,37 +133,6 @@ add_task(async function test_addon_report_data() {
await extension3.unload();
});
add_task(async function test_report_on_not_installed_addon() {
Services.telemetry.clearEvents();
// Make sure that the AMO addons details API endpoint is going to
// return a 404 status for the not installed addon.
amoAddonDetailsMap.delete(ADDON_ID);
await assertRejectsAbuseReportError(
AbuseReporter.createAbuseReport(ADDON_ID, REPORT_OPTIONS),
"ERROR_ADDON_NOTFOUND"
);
TelemetryTestUtils.assertEvents(
[
{
object: REPORT_OPTIONS.reportEntryPoint,
value: ADDON_ID,
extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" },
},
{
object: REPORT_OPTIONS.reportEntryPoint,
value: ADDON_ID,
extra: { error_type: "ERROR_ADDON_NOTFOUND" },
},
],
TELEMETRY_EVENTS_FILTERS
);
Services.telemetry.clearEvents();
});
// This tests verifies how the addon installTelemetryInfo values are being
// normalized into the addon_install_source and addon_install_method
// expected by the API endpoint.
@ -379,526 +221,3 @@ add_task(async function test_normalized_addon_install_source_and_method() {
await assertAddonInstallMethod(test, expect);
}
});
add_task(async function test_report_create_and_submit() {
Services.telemetry.clearEvents();
// Override the test api server request handler, to be able to
// intercept the submittions to the test api server.
let reportSubmitted;
apiRequestHandler = ({ data, request, response }) => {
reportSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
const { addon, extension } = await installTestExtension();
const reportEntryPoint = "menu";
const report = await AbuseReporter.createAbuseReport(ADDON_ID, {
reportEntryPoint,
});
equal(report.addon, addon, "Got the expected addon property");
equal(
report.reportEntryPoint,
reportEntryPoint,
"Got the expected reportEntryPoint"
);
const baseReportData = await AbuseReporter.getReportData(addon);
const reportProperties = {
message: "test message",
reason: "test-reason",
};
info("Submitting report");
report.setMessage(reportProperties.message);
report.setReason(reportProperties.reason);
await report.submit();
const expectedEntries = Object.entries({
report_entry_point: reportEntryPoint,
...baseReportData,
...reportProperties,
});
for (const [expectedKey, expectedValue] of expectedEntries) {
equal(
reportSubmitted[expectedKey],
expectedValue,
`Got the expected submitted value for "${expectedKey}"`
);
}
TelemetryTestUtils.assertEvents(
[
{
object: reportEntryPoint,
value: ADDON_ID,
extra: { addon_type: "extension" },
},
],
TELEMETRY_EVENTS_FILTERS
);
await extension.unload();
});
add_task(async function test_error_recent_submit() {
Services.telemetry.clearEvents();
clearAbuseReportState();
let reportSubmitted;
apiRequestHandler = ({ data, request, response }) => {
reportSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
const { extension } = await installTestExtension();
const report = await AbuseReporter.createAbuseReport(ADDON_ID, {
reportEntryPoint: "uninstall",
});
const { extension: extension2 } = await installTestExtension({
manifest: {
browser_specific_settings: { gecko: { id: ADDON_ID2 } },
name: "Test Extension2",
},
});
const report2 = await AbuseReporter.createAbuseReport(
ADDON_ID2,
REPORT_OPTIONS
);
// Submit the two reports in fast sequence.
report.setReason("reason1");
report2.setReason("reason2");
await report.submit();
await assertRejectsAbuseReportError(report2.submit(), "ERROR_RECENT_SUBMIT");
equal(
reportSubmitted.reason,
"reason1",
"Server only received the data from the first submission"
);
TelemetryTestUtils.assertEvents(
[
{
object: "uninstall",
value: ADDON_ID,
extra: { addon_type: "extension" },
},
{
object: REPORT_OPTIONS.reportEntryPoint,
value: ADDON_ID2,
extra: {
addon_type: "extension",
error_type: "ERROR_RECENT_SUBMIT",
},
},
],
TELEMETRY_EVENTS_FILTERS
);
await extension.unload();
await extension2.unload();
});
add_task(async function test_submission_server_error() {
const { extension } = await installTestExtension();
async function testErrorCode({
responseStatus,
responseText = "",
expectedErrorType,
expectedErrorInfo,
expectRequest = true,
}) {
info(
`Test expected AbuseReportError on response status "${responseStatus}"`
);
Services.telemetry.clearEvents();
clearAbuseReportState();
let requestReceived = false;
apiRequestHandler = ({ request, response }) => {
requestReceived = true;
response.setStatusLine(request.httpVersion, responseStatus, "Error");
response.write(responseText);
};
const report = await AbuseReporter.createAbuseReport(
ADDON_ID,
REPORT_OPTIONS
);
report.setReason("a-reason");
const promiseSubmit = report.submit();
if (typeof expectedErrorType === "string") {
// Assert a specific AbuseReportError errorType.
await assertRejectsAbuseReportError(
promiseSubmit,
expectedErrorType,
expectedErrorInfo
);
} else {
// Assert on a given Error class.
await Assert.rejects(promiseSubmit, expectedErrorType);
}
equal(
requestReceived,
expectRequest,
`${expectRequest ? "" : "Not "}received a request as expected`
);
TelemetryTestUtils.assertEvents(
[
{
object: REPORT_OPTIONS.reportEntryPoint,
value: ADDON_ID,
extra: {
addon_type: "extension",
error_type:
typeof expectedErrorType === "string"
? expectedErrorType
: "ERROR_UNKNOWN",
},
},
],
TELEMETRY_EVENTS_FILTERS
);
}
await testErrorCode({
responseStatus: 500,
responseText: "A server error",
expectedErrorType: "ERROR_SERVER",
expectedErrorInfo: JSON.stringify({
status: 500,
responseText: "A server error",
}),
});
await testErrorCode({
responseStatus: 404,
responseText: "Not found error",
expectedErrorType: "ERROR_CLIENT",
expectedErrorInfo: JSON.stringify({
status: 404,
responseText: "Not found error",
}),
});
// Test response with unexpected status code.
await testErrorCode({
responseStatus: 604,
responseText: "An unexpected status code",
expectedErrorType: "ERROR_UNKNOWN",
expectedErrorInfo: JSON.stringify({
status: 604,
responseText: "An unexpected status code",
}),
});
// Test response status 200 with invalid json data.
await testErrorCode({
responseStatus: 200,
expectedErrorType: /SyntaxError: JSON.parse/,
});
// Test on invalid url.
Services.prefs.setCharPref(
"extensions.abuseReport.url",
"invalid-protocol://abuse-report"
);
await testErrorCode({
expectedErrorType: "ERROR_NETWORK",
expectRequest: false,
});
await extension.unload();
});
add_task(async function set_test_abusereport_url() {
Services.prefs.setCharPref(
"extensions.abuseReport.url",
"http://test.addons.org/api/report/"
);
});
add_task(async function test_submission_aborting() {
Services.telemetry.clearEvents();
clearAbuseReportState();
const { extension } = await installTestExtension();
// override the api request handler with one that is never going to reply.
let receivedRequestsCount = 0;
let resolvePendingResponses;
const waitToReply = new Promise(
resolve => (resolvePendingResponses = resolve)
);
const onRequestReceived = new Promise(resolve => {
apiRequestHandler = ({ request, response }) => {
response.processAsync();
response.setStatusLine(request.httpVersion, 200, "OK");
receivedRequestsCount++;
resolve();
// Keep the request pending until resolvePendingResponses have been
// called.
waitToReply.then(() => {
response.finish();
});
};
});
const report = await AbuseReporter.createAbuseReport(
ADDON_ID,
REPORT_OPTIONS
);
report.setReason("a-reason");
const promiseResult = report.submit();
await onRequestReceived;
Assert.greater(
receivedRequestsCount,
0,
"Got the expected number of requests"
);
Assert.strictEqual(
await Promise.race([promiseResult, Promise.resolve("pending")]),
"pending",
"Submission fetch request should still be pending"
);
report.abort();
await assertRejectsAbuseReportError(promiseResult, "ERROR_ABORTED_SUBMIT");
TelemetryTestUtils.assertEvents(
[
{
object: REPORT_OPTIONS.reportEntryPoint,
value: ADDON_ID,
extra: { addon_type: "extension", error_type: "ERROR_ABORTED_SUBMIT" },
},
],
TELEMETRY_EVENTS_FILTERS
);
await extension.unload();
// Unblock pending requests on the server request handler side, so that the
// test file can shutdown (otherwise the test run will be stuck after this
// task completed).
resolvePendingResponses();
});
add_task(async function test_truncated_string_properties() {
const generateString = len => new Array(len).fill("a").join("");
const LONG_STRINGS_ADDON_ID = "addon-with-long-strings-props@mochi.test";
const { extension } = await installTestExtension({
manifest: {
name: generateString(400),
description: generateString(400),
browser_specific_settings: { gecko: { id: LONG_STRINGS_ADDON_ID } },
},
});
// Override the test api server request handler, to be able to
// intercept the properties actually submitted.
let reportSubmitted;
apiRequestHandler = ({ data, request, response }) => {
reportSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
const report = await AbuseReporter.createAbuseReport(
LONG_STRINGS_ADDON_ID,
REPORT_OPTIONS
);
report.setMessage("fake-message");
report.setReason("fake-reason");
await report.submit();
const expected = {
addon_name: generateString(255),
addon_summary: generateString(255),
};
Assert.deepEqual(
{
addon_name: reportSubmitted.addon_name,
addon_summary: reportSubmitted.addon_summary,
},
expected,
"Got the long strings truncated as expected"
);
await extension.unload();
});
add_task(async function test_report_recommended() {
const NON_RECOMMENDED_ADDON_ID = "non-recommended-addon@mochi.test";
const RECOMMENDED_ADDON_ID = "recommended-addon@mochi.test";
const now = Date.now();
const not_before = new Date(now - 3600000).toISOString();
const not_after = new Date(now + 3600000).toISOString();
const { extension: nonRecommended } = await installTestExtension({
manifest: {
name: "Fake non recommended addon",
browser_specific_settings: { gecko: { id: NON_RECOMMENDED_ADDON_ID } },
},
});
const { extension: recommended } = await installTestExtension({
manifest: {
name: "Fake recommended addon",
browser_specific_settings: { gecko: { id: RECOMMENDED_ADDON_ID } },
},
files: {
"mozilla-recommendation.json": {
addon_id: RECOMMENDED_ADDON_ID,
states: ["recommended"],
validity: { not_before, not_after },
},
},
});
// Override the test api server request handler, to be able to
// intercept the properties actually submitted.
let reportSubmitted;
apiRequestHandler = ({ data, request, response }) => {
reportSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
async function checkReportedSignature(addonId, expectedAddonSignature) {
clearAbuseReportState();
const report = await AbuseReporter.createAbuseReport(
addonId,
REPORT_OPTIONS
);
report.setMessage("fake-message");
report.setReason("fake-reason");
await report.submit();
equal(
reportSubmitted.addon_signature,
expectedAddonSignature,
`Got the expected addon_signature for ${addonId}`
);
}
await checkReportedSignature(NON_RECOMMENDED_ADDON_ID, "signed");
await checkReportedSignature(RECOMMENDED_ADDON_ID, "curated");
await nonRecommended.unload();
await recommended.unload();
});
add_task(async function test_query_amo_details() {
async function assertReportOnAMODetails({ addonId, expectedReport } = {}) {
// Clear last report timestamp and any telemetry event recorded so far.
clearAbuseReportState();
Services.telemetry.clearEvents();
const report = await AbuseReporter.createAbuseReport(addonId, {
reportEntryPoint: "menu",
});
let reportSubmitted;
apiRequestHandler = ({ data, request, response }) => {
reportSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
report.setMessage("fake message");
report.setReason("reason1");
await report.submit();
Assert.deepEqual(
expectedReport,
getProperties(reportSubmitted, Object.keys(expectedReport)),
"Got the expected report properties"
);
// Telemetry recorded for the successfully submitted report.
TelemetryTestUtils.assertEvents(
[
{
object: "menu",
value: addonId,
extra: { addon_type: FAKE_AMO_DETAILS.type },
},
],
TELEMETRY_EVENTS_FILTERS
);
clearAbuseReportState();
}
// Add the expected AMO addons details.
const addonId = "not-installed-addon@mochi.test";
amoAddonDetailsMap.set(addonId, FAKE_AMO_DETAILS);
// Test on the default en-US locale.
Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "en-US");
let locale = Services.locale.appLocaleAsBCP47;
equal(locale, "en-US", "Got the expected app locale set");
let expectedReport = {
addon: addonId,
addon_name: FAKE_AMO_DETAILS.name[locale],
addon_version: FAKE_AMO_DETAILS.current_version.version,
addon_install_source: "not_installed",
addon_install_method: null,
addon_signature: "curated",
};
await assertReportOnAMODetails({ addonId, expectedReport });
// Test with a non-default locale also available in the AMO details.
Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "it-IT");
locale = Services.locale.appLocaleAsBCP47;
equal(locale, "it-IT", "Got the expected app locale set");
expectedReport = {
...expectedReport,
addon_name: FAKE_AMO_DETAILS.name[locale],
};
await assertReportOnAMODetails({ addonId, expectedReport });
// Test with a non-default locale not available in the AMO details.
Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "fr-FR");
locale = Services.locale.appLocaleAsBCP47;
equal(locale, "fr-FR", "Got the expected app locale set");
expectedReport = {
...expectedReport,
// Fallbacks on en-US for non available locales.
addon_name: FAKE_AMO_DETAILS.name["en-US"],
};
await assertReportOnAMODetails({ addonId, expectedReport });
Services.prefs.clearUserPref(PREF_REQUIRED_LOCALE);
amoAddonDetailsMap.clear();
});
add_task(async function test_statictheme_normalized_into_type_theme() {
const themeId = "not-installed-statictheme@mochi.test";
amoAddonDetailsMap.set(themeId, {
...FAKE_AMO_DETAILS,
type: "statictheme",
});
const report = await AbuseReporter.createAbuseReport(themeId, REPORT_OPTIONS);
equal(report.addon.id, themeId, "Got a report for the expected theme id");
equal(report.addon.type, "theme", "Got the expected addon type");
amoAddonDetailsMap.clear();
});