summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/content/abuse-report-panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/content/abuse-report-panel.js')
-rw-r--r--toolkit/mozapps/extensions/content/abuse-report-panel.js886
1 files changed, 886 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/content/abuse-report-panel.js b/toolkit/mozapps/extensions/content/abuse-report-panel.js
new file mode 100644
index 0000000000..d1647ae184
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/abuse-report-panel.js
@@ -0,0 +1,886 @@
+/* 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-report-learnmore": {
+ path: "reporting-extensions-and-themes-abuse",
+ },
+ ".abuse-settings-search-learnmore": {
+ path: "prefs-search",
+ },
+ ".abuse-settings-homepage-learnmore": {
+ path: "prefs-homepage",
+ },
+ ".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 = linkInfo.baseURL
+ ? Services.urlFormatter.formatURL(linkInfo.baseURL)
+ : Services.urlFormatter.formatURLPref("app.support.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;
+ }
+ }
+
+ get LEARNMORE_LINKS() {
+ return Object.keys(LEARNMORE_LINKS);
+ }
+}
+
+// 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 { clientWidth, clientHeight } = await window.promiseDocumentFlushed(
+ () => form
+ );
+ // Resolve promiseReportPanel once the panel completed the initial render
+ // (used in tests).
+ deferredReportPanel.resolve(el);
+ if (
+ window.innerWidth !== clientWidth ||
+ window.innerheight !== clientHeight
+ ) {
+ window.resizeTo(clientWidth, clientHeight);
+ }
+ },
+ { once: true }
+ );
+ el.setAbuseReport(report);
+}