Bug 1544928 - Allow to optionally report an extension on addon uninstall. r=flod,jaws,mstriemer
authorLuca Greco <lgreco@mozilla.com>
Mon, 06 May 2019 18:38:23 +0000
changeset 531569 407fc9248a911ea70e74e44efa709b63af804607
parent 531568 362072ce25dbe0311da24e701c293416c2eda3ec
child 531570 dc5df04e9ce9c61d95b8e1b5e2183c8ebe0fc41b
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, jaws, mstriemer
bugs1544928
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1544928 - Allow to optionally report an extension on addon uninstall. r=flod,jaws,mstriemer Depends on D29121 Differential Revision: https://phabricator.services.mozilla.com/D29122
browser/base/content/browser.js
browser/locales/en-US/chrome/browser/browser.properties
toolkit/mozapps/extensions/content/aboutaddons.js
toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6611,18 +6611,28 @@ function promptRemoveExtension(addon) {
   let brand = document.getElementById("bundle_brand").getString("brandShorterName");
   let {getFormattedString, getString} = gNavigatorBundle;
   let title = getFormattedString("webext.remove.confirmation.title", [name]);
   let message = getFormattedString("webext.remove.confirmation.message", [name, brand]);
   let btnTitle = getString("webext.remove.confirmation.button");
   let {BUTTON_TITLE_IS_STRING: titleString, BUTTON_TITLE_CANCEL: titleCancel,
         BUTTON_POS_0, BUTTON_POS_1, confirmEx} = Services.prompt;
   let btnFlags = BUTTON_POS_0 * titleString + BUTTON_POS_1 * titleCancel;
-  return confirmEx(null, title, message, btnFlags, btnTitle, null, null, null,
-                    {value: 0});
+  let checkboxState = {value: false};
+  let checkboxMessage = null;
+
+  // Enable abuse report checkbox in the remove extension dialog.
+  if (gHtmlAboutAddonsEnabled && gAddonAbuseReportEnabled) {
+    checkboxMessage = getFormattedString("webext.remove.abuseReportCheckbox.message", [
+      document.getElementById("bundle_brand").getString("vendorShortName"),
+    ]);
+  }
+  const result = confirmEx(null, title, message, btnFlags, btnTitle, null, null,
+                           checkboxMessage, checkboxState);
+  return {remove: result === 0, report: checkboxState.value};
 }
 
 var ToolbarContextMenu = {
   updateDownloadsAutoHide(popup) {
     let checkbox = document.getElementById("toolbar-context-autohide-downloads-button");
     let isDownloads = popup.triggerNode && ["downloads-button", "wrapper-downloads-button"].includes(popup.triggerNode.id);
     checkbox.hidden = !isDownloads;
     if (DownloadsButton.autoHideDownloadsButton) {
@@ -6673,25 +6683,30 @@ var ToolbarContextMenu = {
   },
 
   async removeExtensionForContextAction(popup) {
     let id = this._getExtensionId(popup);
     let addon = id && await AddonManager.getAddonByID(id);
     if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
       return;
     }
-    let response = promptRemoveExtension(addon);
+    let {remove, report} = promptRemoveExtension(addon);
     AMTelemetry.recordActionEvent({
       object: "browserAction",
       action: "uninstall",
-      value: response ? "cancelled" : "accepted",
+      value: remove ? "accepted" : "cancelled",
       extra: {addonId: addon.id},
     });
-    if (response == 0) {
-      addon.uninstall();
+    if (remove) {
+      // Leave the extension in pending uninstall if we are also
+      // reporting the add-on.
+      await addon.uninstall(report);
+      if (report) {
+        this.reportExtensionForContextAction(popup, "uninstall");
+      }
     }
   },
 
   async reportExtensionForContextAction(popup, reportEntryPoint) {
     let id = this._getExtensionId(popup);
     let addon = id && await AddonManager.getAddonByID(id);
     if (!addon) {
       return;
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -170,16 +170,19 @@ webext.defaultSearchNo.accessKey=N
 # LOCALIZATION NOTE (webext.remove.confirmation.title)
 # %S is the name of the extension which is about to be removed.
 webext.remove.confirmation.title=Remove %S
 # LOCALIZATION NOTE (webext.remove.confirmation.message)
 # %1$S is the name of the extension which is about to be removed.
 # %2$S is brandShorterName
 webext.remove.confirmation.message=Remove %1$S from %2$S?
 webext.remove.confirmation.button=Remove
+# LOCALIZATION NOTE (webext.remove.abuseReportCheckbox.message)
+# %S is vendorShortName
+webext.remove.abuseReportCheckbox.message=I want to report this extension to %S
 
 # LOCALIZATION NOTE (addonPostInstall.message1)
 # %1$S is replaced with the localized named of the extension that was
 # just installed.
 # %2$S is replaced with the localized name of the application.
 addonPostInstall.message1=%1$S has been added to %2$S.
 
 # LOCALIZATION NOTE (addonDownloadingAndVerifying):
--- a/toolkit/mozapps/extensions/content/aboutaddons.js
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -766,20 +766,27 @@ class AddonCard extends HTMLElement {
           windowRoot.ownerGlobal.openUILinkIn(addon.contributionURL, "tab", {
             triggeringPrincipal:
               Services.scriptSecurityManager.createNullPrincipal({}),
           });
           break;
         case "remove":
           {
             this.panel.hide();
-            let response = windowRoot.ownerGlobal.promptRemoveExtension(addon);
-            if (response == 0) {
+            let {
+              remove, report,
+            } = windowRoot.ownerGlobal.promptRemoveExtension(addon);
+            if (remove) {
               await addon.uninstall(true);
               this.sendEvent("remove");
+              if (report) {
+                openAbuseReport({
+                  addonId: addon.id, reportEntryPoint: "uninstall",
+                });
+              }
             } else {
               this.sendEvent("remove-cancelled");
             }
           }
           break;
         case "expand":
           loadViewFn("detail", this.addon.id);
           break;
--- a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
@@ -4,16 +4,21 @@
 /* eslint max-len: ["error", 80] */
 
 const {
   AbuseReporter,
 } = ChromeUtils.import("resource://gre/modules/AbuseReporter.jsm");
 const {
   AddonTestUtils,
 } = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
+const {
+  ExtensionCommon,
+} = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+
+const {makeWidgetId} = ExtensionCommon;
 
 const ADDON_ID = "test-extension-to-report@mochi.test";
 const REPORT_ENTRY_POINT = "test-entrypoint";
 const BASE_TEST_MANIFEST = {
   name: "Fake extension to report",
   author: "Fake author",
   homepage_url: "https://fake.extension.url/",
   applications: {gecko: {id: ADDON_ID}},
@@ -38,16 +43,31 @@ server.registerPathHandler("/api/report/
 });
 
 function handleSubmitRequest({request, response}) {
   response.setStatusLine(request.httpVersion, 200, "OK");
   response.setHeader("Content-Type", "application/json", false);
   response.write("{}");
 }
 
+function createPromptConfirmEx({remove = false, report = false} = {}) {
+  return (...args) => {
+    const checkboxState = args.pop();
+    const checkboxMessage = args.pop();
+    is(checkboxState && checkboxState.value, false,
+       "checkboxState should be initially false");
+    ok(checkboxMessage,
+       "Got a checkboxMessage in promptService.confirmEx call");
+    // Report checkbox selected.
+    checkboxState.value = report;
+    // Remove accepted.
+    return remove ? 0 : 1;
+  };
+}
+
 async function openAboutAddons(type = "extension") {
   const win = await loadInitialView(type);
   gHtmlAboutAddonsWindow = win;
   gManagerWindow = win.managerWindow;
 }
 
 async function closeAboutAddons() {
   if (gHtmlAboutAddonsWindow) {
@@ -859,8 +879,106 @@ add_task(async function test_trigger_abu
   await promiseAbuseReportRendered(abuseReportEl);
 
   is(abuseReportEl.addon && abuseReportEl.addon.id, extension.id,
      "Abuse Report panel rendered for the expected addonId");
 
   await closeAboutAddons();
   await extension.unload();
 });
+
+add_task(async function test_trigger_abusereport_from_aboutaddons_remove() {
+  const EXT_ID = "test-report-from-aboutaddons-remove@mochi.test";
+  const extension = await installTestExtension(EXT_ID);
+
+  await openAboutAddons();
+  await gManagerWindow.htmlBrowserLoaded;
+
+  const abuseReportFrameEl = getAbuseReportFrame();
+  ok(abuseReportFrameEl.hidden,
+     "Abuse Report frame should be initially hidden");
+
+  const {contentDocument: doc} = gManagerWindow.getHtmlBrowser();
+
+  const addonCard = doc.querySelector(
+    `addon-list addon-card[addon-id="${extension.id}"]`);
+  ok(addonCard, "Got the addon-card for the test extension");
+
+  const removeButton = addonCard.querySelector("[action=remove]");
+  ok(removeButton, "Got the report action for the test extension");
+
+  const onceReportNew = BrowserTestUtils.waitForEvent(
+    abuseReportFrameEl, "abuse-report:new");
+  const onceReportFrameShown = BrowserTestUtils.waitForEvent(
+    abuseReportFrameEl, "abuse-report:frame-shown");
+
+  // Prepare the mocked prompt service.
+  const promptService = mockPromptService();
+  promptService.confirmEx = createPromptConfirmEx({remove: true, report: true});
+
+  info("Click the report action and wait for the 'abuse-report:new' event");
+  removeButton.click();
+  const newReportEvent = await onceReportNew;
+
+  Assert.deepEqual(newReportEvent.detail, {
+    addonId: extension.id,
+    reportEntryPoint: "uninstall",
+  }, "Got the expected details in the 'abuse-report:new' event");
+
+  info("Wait for the abuse report frame to be visible");
+  await onceReportFrameShown;
+  ok(!abuseReportFrameEl.hidden,
+    "Abuse Report frame should be visible");
+
+  info("Wait for the abuse report panel to be rendered");
+  const abuseReportEl = await abuseReportFrameEl.promiseAbuseReport;
+  await promiseAbuseReportRendered(abuseReportEl);
+
+  is(abuseReportEl.addon && abuseReportEl.addon.id, extension.id,
+     "Abuse Report panel rendered for the expected addonId");
+
+  await closeAboutAddons();
+  await extension.unload();
+});
+
+add_task(async function test_trigger_abusereport_from_browserAction_remove() {
+  const EXT_ID = "test-report-from-browseraction-remove@mochi.test";
+  const extension = await installTestExtension(EXT_ID, "extension", {
+    browser_action: {},
+  });
+
+  // Prepare the mocked prompt service.
+  const promptService = mockPromptService();
+  promptService.confirmEx = createPromptConfirmEx({remove: true, report: true});
+
+  info(`Open browserAction context menu in toolbar context menu`);
+  let buttonId = `${makeWidgetId(EXT_ID)}-browser-action`;
+  const menu = document.getElementById("toolbar-context-menu");
+  const node = document.getElementById(CSS.escape(buttonId));
+  const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+  EventUtils.synthesizeMouseAtCenter(node, {type: "contextmenu"});
+  await shown;
+
+  info(`Clicking on "Remove Extension" context menu item`);
+  let removeExtension = menu.querySelector(
+    ".customize-context-removeExtension");
+  removeExtension.click();
+
+  // Wait about:addons to be loaded.
+  const browser = gBrowser.selectedBrowser;
+  await BrowserTestUtils.browserLoaded(browser);
+  is(browser.currentURI.spec, "about:addons",
+    "about:addons tab currently selected");
+  menu.hidePopup();
+
+  const abuseReportFrame = browser.contentDocument
+    .querySelector("addon-abuse-report-xulframe");
+
+  ok(!abuseReportFrame.hidden,
+     "Abuse report frame has the expected visibility");
+  is(abuseReportFrame.report.addon.id, EXT_ID,
+    "Abuse report frame has the expected addon id");
+  is(abuseReportFrame.report.reportEntryPoint, "uninstall",
+    "Abuse report frame has the expected reportEntryPoint");
+
+  await closeAboutAddons();
+  await extension.unload();
+});