Bug 648675 - Allow comments and URL opt-in in content/plugin crash UI. r=dolske,ted
authorDrew Willcoxon <adw@mozilla.com>
Thu, 14 Feb 2013 15:57:50 -0800
changeset 121987 528411b6f62860ccb9cbb996033a4d84965771ed
parent 121986 aa98a13a5f7356c4619163291928045c199a880b
child 121988 ec601ae4a64d18a723bddb1399d2e71d040282b0
push id24314
push userryanvm@gmail.com
push dateFri, 15 Feb 2013 14:39:46 +0000
treeherdermozilla-central@326c5e4868fe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdolske, ted
bugs648675
milestone21.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 648675 - Allow comments and URL opt-in in content/plugin crash UI. r=dolske,ted
browser/base/content/browser-plugins.js
browser/base/content/test/Makefile.in
browser/base/content/test/browser_pluginCrashCommentAndURL.js
browser/base/content/test/pluginCrashCommentAndURL.html
dom/plugins/test/mochitest/test_crash_submit.xul
dom/plugins/test/mochitest/test_hang_submit.xul
modules/libpref/src/init/all.js
toolkit/crashreporter/CrashSubmit.jsm
toolkit/locales/en-US/chrome/mozapps/plugins/plugins.dtd
toolkit/mozapps/plugins/content/pluginProblem.xml
toolkit/mozapps/plugins/content/pluginProblemContent.css
toolkit/themes/pinstripe/mozapps/plugins/pluginProblem.css
toolkit/themes/winstripe/mozapps/plugins/pluginProblem.css
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -3,16 +3,21 @@
 # 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/.
 
 var gPluginHandler = {
   PLUGIN_SCRIPTED_STATE_NONE: 0,
   PLUGIN_SCRIPTED_STATE_FIRED: 1,
   PLUGIN_SCRIPTED_STATE_DONE: 2,
 
+  getPluginUI: function (plugin, className) {
+    return plugin.ownerDocument.
+           getAnonymousElementByAttribute(plugin, "class", className);
+  },
+
 #ifdef MOZ_CRASHREPORTER
   get CrashSubmit() {
     delete this.CrashSubmit;
     Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
     return this.CrashSubmit;
   },
 #endif
 
@@ -404,21 +409,26 @@ var gPluginHandler = {
 
   // Callback for user clicking on the link in a click-to-play plugin
   // (where the plugin has an update)
   openPluginUpdatePage: function (aEvent) {
     openURL(Services.urlFormatter.formatURLPref("plugins.update.url"));
   },
 
 #ifdef MOZ_CRASHREPORTER
-  // Callback for user clicking "submit a report" link
-  submitReport : function(pluginDumpID, browserDumpID) {
-    // The crash reporter wants a DOM element it can append an IFRAME to,
-    // which it uses to submit a form. Let's just give it gBrowser.
-    this.CrashSubmit.submit(pluginDumpID);
+  submitReport: function submitReport(pluginDumpID, browserDumpID, plugin) {
+    let keyVals = {};
+    if (plugin) {
+      let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
+      if (userComment)
+        keyVals.PluginUserComment = userComment;
+      if (this.getPluginUI(plugin, "submitURLOptIn").checked)
+        keyVals.PluginContentURL = plugin.ownerDocument.URL;
+    }
+    this.CrashSubmit.submit(pluginDumpID, { extraExtraKeyVals: keyVals });
     if (browserDumpID)
       this.CrashSubmit.submit(browserDumpID);
   },
 #endif
 
   // Callback for user clicking a "reload page" link
   reloadPage: function (browser) {
     browser.reload();
@@ -932,33 +942,36 @@ var gPluginHandler = {
     if (submittedReport) { // submitReports && !doPrompt, handled in observer
       status = "submitted";
     }
     else if (!submitReports && !doPrompt) {
       status = "noSubmit";
     }
     else { // doPrompt
       status = "please";
-      // XXX can we make the link target actually be blank?
-      let pleaseLink = doc.getAnonymousElementByAttribute(
-                            plugin, "class", "pleaseSubmitLink");
-      this.addLinkClickCallback(pleaseLink, "submitReport",
-                                pluginDumpID, browserDumpID);
+      this.getPluginUI(plugin, "submitButton").addEventListener("click",
+        function (event) {
+          if (event.button != 0 || !event.isTrusted)
+            return;
+          this.submitReport(pluginDumpID, browserDumpID, plugin);
+          pref.setBoolPref("", optInCB.checked);
+        }.bind(this));
+      let optInCB = this.getPluginUI(plugin, "submitURLOptIn");
+      let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL");
+      optInCB.checked = pref.getBoolPref("");
     }
 
     // If we don't have a minidumpID, we can't (or didn't) submit anything.
     // This can happen if the plugin is killed from the task manager.
     if (!pluginDumpID) {
         status = "noReport";
     }
 
     statusDiv.setAttribute("status", status);
 
-    let bottomLinks = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgBottomLinks");
-    bottomLinks.style.display = "block";
     let helpIcon = doc.getAnonymousElementByAttribute(plugin, "class", "helpIcon");
     this.addLinkClickCallback(helpIcon, "openHelpPage");
 
     // If we're showing the link to manually trigger report submission, we'll
     // want to be able to update all the instances of the UI for this crash to
     // show an updated message when a report is submitted.
     if (doPrompt) {
       let observer = {
@@ -985,41 +998,52 @@ var gPluginHandler = {
       // it from being GC. But I don't want to manually manage the reference's
       // lifetime (which should be no greater than the page).
       // Clever solution? Use a closue with an event listener on the document.
       // When the doc goes away, so do the listener references and the closure.
       doc.addEventListener("mozCleverClosureHack", observer, false);
     }
 #endif
 
-    let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgCrashed");
+    let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msgCrashedText");
     crashText.textContent = messageString;
 
     let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document);
 
     let link = doc.getAnonymousElementByAttribute(plugin, "class", "reloadLink");
     this.addLinkClickCallback(link, "reloadPage", browser);
 
     let notificationBox = gBrowser.getNotificationBox(browser);
 
+    let isShowing = true;
+
     // Is the <object>'s size too small to hold what we want to show?
     if (this.isTooSmall(plugin, overlay)) {
-        // Hide the overlay's contents. Use visibility style, so that it
-        // doesn't collapse down to 0x0.
+      // First try hiding the crash report submission UI.
+      statusDiv.removeAttribute("status");
+
+      if (this.isTooSmall(plugin, overlay)) {
+        // Hide the overlay's contents. Use visibility style, so that it doesn't
+        // collapse down to 0x0.
         overlay.style.visibility = "hidden";
-        // If another plugin on the page was large enough to show our UI, we
-        // don't want to show a notification bar.
-        if (!doc.mozNoPluginCrashedNotification)
-          showNotificationBar(pluginDumpID, browserDumpID);
+        isShowing = false;
+      }
+    }
+
+    if (isShowing) {
+      // If a previous plugin on the page was too small and resulted in adding a
+      // notification bar, then remove it because this plugin instance it big
+      // enough to serve as in-content notification.
+      hideNotificationBar();
+      doc.mozNoPluginCrashedNotification = true;
     } else {
-        // If a previous plugin on the page was too small and resulted in
-        // adding a notification bar, then remove it because this plugin
-        // instance it big enough to serve as in-content notification.
-        hideNotificationBar();
-        doc.mozNoPluginCrashedNotification = true;
+      // If another plugin on the page was large enough to show our UI, we don't
+      // want to show a notification bar.
+      if (!doc.mozNoPluginCrashedNotification)
+        showNotificationBar(pluginDumpID, browserDumpID);
     }
 
     function hideNotificationBar() {
       let notification = notificationBox.getNotificationWithValue("plugin-crashed");
       if (notification)
         notificationBox.removeNotification(notification, true);
     }
 
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -297,16 +297,18 @@ endif
                  browser_utilityOverlay.js \
                  browser_bug676619.js \
                  download_page.html \
                  browser_URLBarSetURI.js \
                  browser_bookmark_titles.js \
                  browser_pageInfo_plugins.js \
                  browser_pageInfo.js \
                  feed_tab.html \
+                 browser_pluginCrashCommentAndURL.js \
+                 pluginCrashCommentAndURL.html \
                  $(NULL)
 
 ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 _BROWSER_FILES += \
 		browser_bug462289.js \
 		$(NULL)
 else
 _BROWSER_FILES += \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_pluginCrashCommentAndURL.js
@@ -0,0 +1,154 @@
+/* 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/. */
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const CRASH_URL = "http://example.com/browser/browser/base/content/test/pluginCrashCommentAndURL.html";
+
+const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
+
+function test() {
+  // Crashing the plugin takes up a lot of time, so extend the test timeout.
+  requestLongerTimeout(runs.length);
+  waitForExplicitFinish();
+
+  // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
+  // crash reports.  This test needs them enabled.  The test also needs a mock
+  // report server, and fortunately one is already set up by toolkit/
+  // crashreporter/test/Makefile.in.  Assign its URL to MOZ_CRASHREPORTER_URL,
+  // which CrashSubmit.jsm uses as a server override.
+  let env = Cc["@mozilla.org/process/environment;1"].
+            getService(Components.interfaces.nsIEnvironment);
+  let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
+  let serverURL = env.get("MOZ_CRASHREPORTER_URL");
+  env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+  env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+  let tab = gBrowser.loadOneTab("about:blank", { inBackground: false });
+  let browser = gBrowser.getBrowserForTab(tab);
+  browser.addEventListener("PluginCrashed", onCrash, false);
+  Services.obs.addObserver(onSubmitStatus, "crash-report-status", false);
+
+  registerCleanupFunction(function cleanUp() {
+    env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+    env.set("MOZ_CRASHREPORTER_URL", serverURL);
+    gBrowser.selectedBrowser.removeEventListener("PluginCrashed", onCrash,
+                                                 false);
+    Services.obs.removeObserver(onSubmitStatus, "crash-report-status");
+    gBrowser.removeCurrentTab();
+  });
+
+  doNextRun();
+}
+
+let runs = [
+  {
+    shouldSubmissionUIBeVisible: true,
+    comment: "",
+    urlOptIn: false,
+  },
+  {
+    shouldSubmissionUIBeVisible: true,
+    comment: "a test comment",
+    urlOptIn: true,
+  },
+  {
+    width: 300,
+    height: 300,
+    shouldSubmissionUIBeVisible: false,
+  },
+];
+
+let currentRun = null;
+
+function doNextRun() {
+  try {
+    if (!runs.length) {
+      finish();
+      return;
+    }
+    currentRun = runs.shift();
+    let args = ["width", "height"].reduce(function (memo, arg) {
+      if (arg in currentRun)
+        memo[arg] = currentRun[arg];
+      return memo;
+    }, {});
+    gBrowser.loadURI(CRASH_URL + "?" +
+                     encodeURIComponent(JSON.stringify(args)));
+    // And now wait for the crash.
+  }
+  catch (err) {
+    failWithException(err);
+    finish();
+  }
+}
+
+function onCrash() {
+  try {
+    let plugin = gBrowser.contentDocument.getElementById("plugin");
+    let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
+    let style =
+      gBrowser.contentWindow.getComputedStyle(elt("msg msgPleaseSubmit"));
+    is(style.display,
+       currentRun.shouldSubmissionUIBeVisible ? "block" : "none",
+       "Submission UI visibility should be correct");
+    if (!currentRun.shouldSubmissionUIBeVisible) {
+      // Done with this run.
+      doNextRun();
+      return;
+    }
+    elt("submitComment").value = currentRun.comment;
+    elt("submitURLOptIn").checked = currentRun.urlOptIn;
+    elt("submitButton").click();
+    // And now wait for the submission status notification.
+  }
+  catch (err) {
+    failWithException(err);
+    doNextRun();
+  }
+}
+
+function onSubmitStatus(subj, topic, data) {
+  try {
+    // Wait for success or failed, doesn't matter which.
+    if (data != "success" && data != "failed")
+      return;
+
+    let extra = getPropertyBagValue(subj.QueryInterface(Ci.nsIPropertyBag),
+                                    "extra");
+    ok(extra instanceof Ci.nsIPropertyBag, "Extra data should be property bag");
+
+    let val = getPropertyBagValue(extra, "PluginUserComment");
+    if (currentRun.comment)
+      is(val, currentRun.comment,
+         "Comment in extra data should match comment in textbox");
+    else
+      ok(val === undefined,
+         "Comment should be absent from extra data when textbox is empty");
+
+    val = getPropertyBagValue(extra, "PluginContentURL");
+    if (currentRun.urlOptIn)
+      is(val, gBrowser.currentURI.spec,
+         "URL in extra data should match browser URL when opt-in checked");
+    else
+      ok(val === undefined,
+         "URL should be absent from extra data when opt-in not checked");
+  }
+  catch (err) {
+    failWithException(err);
+  }
+  doNextRun();
+}
+
+function getPropertyBagValue(bag, key) {
+  try {
+    var val = bag.getProperty(key);
+  }
+  catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
+  return val;
+}
+
+function failWithException(err) {
+  ok(false, "Uncaught exception: " + err + "\n" + err.stack);
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/pluginCrashCommentAndURL.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <script type="text/javascript">
+      function crash() {
+        var plugin = document.getElementById("plugin");
+        var argStr = decodeURIComponent(window.location.search.substr(1));
+        if (argStr) {
+          var args = JSON.parse(argStr);
+          for (var key in args)
+            plugin.setAttribute(key, args[key]);
+        }
+        try {
+          plugin.crash();
+        }
+        catch (err) {}
+      }
+    </script>
+  </head>
+  <body onload="crash();">
+    <embed id="plugin" type="application/x-test"
+           width="400" height="400"
+           drawmode="solid" color="FF00FFFF">
+    </embed>
+  </body>
+</html>
--- a/dom/plugins/test/mochitest/test_crash_submit.xul
+++ b/dom/plugins/test/mochitest/test_crash_submit.xul
@@ -81,20 +81,21 @@ var testObserver = {
   }
 };
 
 
 function onPluginCrashed(aEvent) {
   ok(true, "Plugin crashed notification received");
   is(aEvent.type, "PluginCrashed", "event is correct type");
 
-  let pleaseLink = document.getAnonymousElementByAttribute(
-                            aEvent.target, "class", "pleaseSubmitLink");
+  let submitButton = document.getAnonymousElementByAttribute(aEvent.target,
+                                                             "class",
+                                                             "submitButton");
   // try to submit this report
-  sendMouseEvent({type:'click'}, pleaseLink, window);
+  sendMouseEvent({type:'click'}, submitButton, window);
 }
 
 function runTests() {
   if (!SimpleTest.testPluginIsOOP()) {
     todo(false, "Skipping this test when test plugin is not OOP.");
     SimpleTest.finish();
     return;
   }
--- a/dom/plugins/test/mochitest/test_hang_submit.xul
+++ b/dom/plugins/test/mochitest/test_hang_submit.xul
@@ -107,20 +107,21 @@ var testObserver = {
     throw Components.results.NS_NOINTERFACE;
   }
 };
 
 function onPluginCrashed(aEvent) {
   ok(true, "Plugin crashed notification received");
   is(aEvent.type, "PluginCrashed", "event is correct type");
 
-  let pleaseLink = document.getAnonymousElementByAttribute(
-                            aEvent.target, "class", "pleaseSubmitLink");
+  let submitButton = document.getAnonymousElementByAttribute(aEvent.target,
+                                                             "class",
+                                                             "submitButton");
   // try to submit this report
-  sendMouseEvent({type:'click'}, pleaseLink, window);
+  sendMouseEvent({type:'click'}, submitButton, window);
 }
 
 function runTests() {
   if (!SimpleTest.testPluginIsOOP()) {
     todo(false, "Skipping this test when test plugin is not OOP.");
     SimpleTest.finish();
     return;
   }
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -1810,16 +1810,17 @@ pref("dom.ipc.tabs.shutdownTimeoutSecs",
 
 #ifdef XP_WIN
 // Disable oopp for java on windows. They run their own
 // process isolation which conflicts with our implementation.
 pref("dom.ipc.plugins.java.enabled", false);
 #endif
 
 pref("dom.ipc.plugins.flash.subprocess.crashreporter.enabled", true);
+pref("dom.ipc.plugins.reportCrashURL", true);
 
 pref("dom.ipc.processCount", 1);
 
 // Enable the use of display-lists for SVG hit-testing and painting.
 pref("svg.display-lists.hit-testing.enabled", true);
 pref("svg.display-lists.painting.enabled", true);
 
 // Is support for the SVG 2 paint-order property enabled?
--- a/toolkit/crashreporter/CrashSubmit.jsm
+++ b/toolkit/crashreporter/CrashSubmit.jsm
@@ -181,22 +181,24 @@ function writeSubmittedReport(crashID, v
      data += "\n" + strings.reporturl.replace("%s", viewURL);
 
   os.writeString(data);
   os.close();
   fstream.close();
 }
 
 // the Submitter class represents an individual submission.
-function Submitter(id, submitSuccess, submitError, noThrottle) {
+function Submitter(id, submitSuccess, submitError, noThrottle,
+                   extraExtraKeyVals) {
   this.id = id;
   this.successCallback = submitSuccess;
   this.errorCallback = submitError;
   this.noThrottle = noThrottle;
   this.additionalDumps = [];
+  this.extraKeyVals = extraExtraKeyVals || {};
 }
 
 Submitter.prototype = {
   submitSuccess: function Submitter_submitSuccess(ret)
   {
     if (!ret.CrashID) {
       this.notifyStatus(FAILED);
       this.cleanup();
@@ -233,47 +235,46 @@ Submitter.prototype = {
     // remove this object from the list of active submissions
     let idx = CrashSubmit._activeSubmissions.indexOf(this);
     if (idx != -1)
       CrashSubmit._activeSubmissions.splice(idx, 1);
   },
 
   submitForm: function Submitter_submitForm()
   {
-    let reportData = parseKeyValuePairsFromFile(this.extra);
-    if (!('ServerURL' in reportData)) {
+    if (!('ServerURL' in this.extraKeyVals)) {
       return false;
     }
-
-    let serverURL = reportData.ServerURL;
-    delete reportData.ServerURL;
+    let serverURL = this.extraKeyVals.ServerURL;
 
     // Override the submission URL from the environment or prefs.
 
     var envOverride = Cc['@mozilla.org/process/environment;1'].
       getService(Ci.nsIEnvironment).get("MOZ_CRASHREPORTER_URL");
     if (envOverride != '') {
       serverURL = envOverride;
     }
-    else if ('PluginHang' in reportData) {
+    else if ('PluginHang' in this.extraKeyVals) {
       try {
         serverURL = Services.prefs.
           getCharPref("toolkit.crashreporter.pluginHangSubmitURL");
       } catch(e) { }
     }
 
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
               .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("POST", serverURL, true);
 
     let formData = Cc["@mozilla.org/files/formdata;1"]
                    .createInstance(Ci.nsIDOMFormData);
-    // add the other data
-    for (let [name, value] in Iterator(reportData)) {
-      formData.append(name, value);
+    // add the data
+    for (let [name, value] in Iterator(this.extraKeyVals)) {
+      if (name != "ServerURL") {
+        formData.append(name, value);
+      }
     }
     if (this.noThrottle) {
       // tell the server not to throttle this, since it was manually submitted
       formData.append("Throttleable", "0");
     }
     // add the minidumps
     formData.append("upload_file_minidump", File(this.dump.path));
     if (this.additionalDumps.length > 0) {
@@ -306,16 +307,23 @@ Submitter.prototype = {
   {
     let propBag = Cc["@mozilla.org/hash-property-bag;1"].
                   createInstance(Ci.nsIWritablePropertyBag2);
     propBag.setPropertyAsAString("minidumpID", this.id);
     if (status == SUCCESS) {
       propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
     }
 
+    let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].
+                          createInstance(Ci.nsIWritablePropertyBag2);
+    for (let key in this.extraKeyVals) {
+      extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
+    }
+    propBag.setPropertyAsInterface("extra", extraKeyValsBag);
+
     Services.obs.notifyObservers(propBag, "crash-report-status", status);
 
     switch (status) {
       case SUCCESS:
         if (this.successCallback)
           this.successCallback(this.id, ret);
         break;
       case FAILED:
@@ -331,20 +339,26 @@ Submitter.prototype = {
   {
     let [dump, extra] = getPendingMinidump(this.id);
     if (!dump.exists() || !extra.exists()) {
       this.notifyStatus(FAILED);
       this.cleanup();
       return false;
     }
 
-    let reportData = parseKeyValuePairsFromFile(extra);
+    let extraKeyVals = parseKeyValuePairsFromFile(extra);
+    for (let key in extraKeyVals) {
+      if (!(key in this.extraKeyVals)) {
+        this.extraKeyVals[key] = extraKeyVals[key];
+      }
+    }
+
     let additionalDumps = [];
-    if ("additional_minidumps" in reportData) {
-      let names = reportData.additional_minidumps.split(',');
+    if ("additional_minidumps" in this.extraKeyVals) {
+      let names = this.extraKeyVals.additional_minidumps.split(',');
       for (let name of names) {
         let [dump, extra] = getPendingMinidump(this.id + "-" + name);
         if (!dump.exists()) {
           this.notifyStatus(FAILED);
           this.cleanup();
           return false;
         }
         additionalDumps.push({'name': name, 'dump': dump});
@@ -386,39 +400,48 @@ this.CrashSubmit = {
    *          report fails to submit: the id that was passed to this
    *          function.
    *        - noThrottle
    *          If true, this crash report should be submitted with
    *          an extra parameter of "Throttleable=0" indicating that
    *          it should be processed right away. This should be set
    *          when the report is being submitted and the user expects
    *          to see the results immediately. Defaults to false.
+   *        - extraExtraKeyVals
+   *          An object whose key-value pairs will be merged with the data from
+   *          the ".extra" file submitted with the report.  The properties of
+   *          this object will override properties of the same name in the
+   *          .extra file.
    *
    * @return true if the submission began successfully, or false if
    *         it failed for some reason. (If the dump file does not
    *         exist, for example.)
    */
   submit: function CrashSubmit_submit(id, params)
   {
     params = params || {};
     let submitSuccess = null;
     let submitError = null;
     let noThrottle = false;
+    let extraExtraKeyVals = null;
 
     if ('submitSuccess' in params)
       submitSuccess = params.submitSuccess;
     if ('submitError' in params)
       submitError = params.submitError;
     if ('noThrottle' in params)
       noThrottle = params.noThrottle;
+    if ('extraExtraKeyVals' in params)
+      extraExtraKeyVals = params.extraExtraKeyVals;
 
     let submitter = new Submitter(id,
                                   submitSuccess,
                                   submitError,
-                                  noThrottle);
+                                  noThrottle,
+                                  extraExtraKeyVals);
     CrashSubmit._activeSubmissions.push(submitter);
     return submitter.submit();
   },
 
   /**
    * Get the list of pending crash IDs.
    *
    * @return an array of string, each being an ID as
--- a/toolkit/locales/en-US/chrome/mozapps/plugins/plugins.dtd
+++ b/toolkit/locales/en-US/chrome/mozapps/plugins/plugins.dtd
@@ -51,11 +51,13 @@
 <!ENTITY reloadPlugin.post                                   " to try again.">
 <!-- LOCALIZATION NOTE (report.please): This and the other report.* strings should be as short as possible, ideally 2-3 words. -->
 <!ENTITY report.please                                       "Send crash report">
 <!ENTITY report.submitting                                   "Sending report…">
 <!ENTITY report.submitted                                    "Crash report sent.">
 <!ENTITY report.disabled                                     "Crash reporting disabled.">
 <!ENTITY report.failed                                       "Submission failed.">
 <!ENTITY report.unavailable                                  "No report available.">
+<!ENTITY report.comment                                      "Add a comment (comments are publicly visible)">
+<!ENTITY report.pageURL                                      "Include the page's URL">
 
 <!ENTITY plugin.file                                         "File">
 <!ENTITY plugin.mimeTypes                                    "MIME Types">
--- a/toolkit/mozapps/plugins/content/pluginProblem.xml
+++ b/toolkit/mozapps/plugins/content/pluginProblem.xml
@@ -32,37 +32,46 @@
                 <html:div class="msg msgUnsupported">&missingPlugin;</html:div>
                 <html:div class="msg msgUnsupportedPlatform">&unsupportedPlatform.pre;<html:a class="unsupportedLearnMoreLink" href="" target="_blank">&unsupportedPlatform.learnMore;</html:a>&unsupportedPlatform.post;</html:div>
                 <html:div class="msg msgTapToPlay">&tapToPlayPlugin;</html:div>
                 <html:div class="msg msgVulnerabilityStatus" anonid="vulnerabilityStatus"><!-- set at runtime --></html:div>
                 <html:div class="msg msgCheckForUpdates"><html:a class="checkForUpdatesLink" href="">&checkForUpdates;</html:a></html:div>
                 <html:div class="msg msgClickToPlay">&clickToActivatePlugin;</html:div>
                 <html:div class="msg msgDisabled">&disabledPlugin;</html:div>
                 <html:div class="msg msgBlocked">&blockedPlugin.label;</html:div>
-                <html:div class="msg msgCrashed"><!-- set at runtime --></html:div>
+                <html:div class="msg msgCrashed">
+                    <html:div class="msgCrashedText"><!-- set at runtime --></html:div>
+                    <!-- link href set at runtime -->
+                    <html:div class="msgReload">&reloadPlugin.pre;<html:a class="reloadLink" href="">&reloadPlugin.middle;</html:a>&reloadPlugin.post;</html:div>
+                </html:div>
 
                 <html:div class="installStatus">
                     <html:div class="msg msgInstallPlugin"><html:a class="installPluginLink" href="">&installPlugin;</html:a></html:div>
                 </html:div>
                 <html:div class="msg msgManagePlugins"><html:a class="managePluginsLink" href="">&managePlugins;</html:a></html:div>
                 <html:div class="submitStatus">
-                    <!-- links set at runtime -->
-                    <html:div class="msg msgPleaseSubmit"><html:a class="pleaseSubmitLink" href="">&report.please;</html:a></html:div>
+                    <html:div class="msg msgPleaseSubmit">
+                        <html:textarea class="submitComment"
+                                       placeholder="&report.comment;"/>
+                        <html:div class="submitURLOptInBox">
+                            <html:label><html:input class="submitURLOptIn" type="checkbox"/> &report.pageURL;</html:label>
+                        </html:div>
+                        <html:div class="submitButtonBox">
+                            <html:span class="helpIcon" role="link"/>
+                            <html:input class="submitButton" type="button"
+                                        value="&report.please;"/>
+                        </html:div>
+                    </html:div>
                     <html:div class="msg msgSubmitting">&report.submitting;<html:span class="throbber"> </html:span></html:div>
                     <html:div class="msg msgSubmitted">&report.submitted;</html:div>
                     <html:div class="msg msgNotSubmitted">&report.disabled;</html:div>
                     <html:div class="msg msgSubmitFailed">&report.failed;</html:div>
                     <html:div class="msg msgNoCrashReport">&report.unavailable;</html:div>
-                    <!-- link href set at runtime -->
-                    <html:div class="msg msgReload">&reloadPlugin.pre;<html:a class="reloadLink" href="">&reloadPlugin.middle;</html:a>&reloadPlugin.post;</html:div>
                 </html:div>
                 <xul:spacer flex="1"/>
-                <html:div class="msg msgBottomLinks">
-                    <html:span class="helpIcon" role="link"/>
-                </html:div>
             </xul:vbox>
         </xul:vbox>
         <html:div class="previewPluginContent"><!-- iframe and its src will be set at runtime --></html:div>
         <html:div style="display:none;"><children/></html:div>
     </content>
     <implementation>
       <constructor>
         // Notify browser-plugins.js that we were attached
--- a/toolkit/mozapps/plugins/content/pluginProblemContent.css
+++ b/toolkit/mozapps/plugins/content/pluginProblemContent.css
@@ -99,26 +99,23 @@ html|applet:not([height]), html|applet[h
 :-moz-handler-crashed .msgCrashed {
   display: block;
 }
 
 .installStatus[status="ready"] .msgInstallPlugin {
   display: block;
 }
 
+.submitStatus[status] {
+  display: -moz-box;
+  -moz-box-align: center;
+  -moz-box-pack: center;
+  height: 160px;
+}
+
 .submitStatus[status="noReport"]   .msgNoCrashReport,
 .submitStatus[status="please"]     .msgPleaseSubmit,
 .submitStatus[status="noSubmit"]   .msgNotSubmitted,
 .submitStatus[status="submitting"] .msgSubmitting,
 .submitStatus[status="success"]    .msgSubmitted,
-.submitStatus[status="failed"]     .msgSubmitFailed,
-.submitStatus[status]:not([status="please"]) .msgReload {
+.submitStatus[status="failed"]     .msgSubmitFailed {
   display: block;
 }
-.submitStatus[status="please"] .msgReload {
-  /* Take up space when invisible, so stuff doesn't shift upon reveal. */
-  display: block;
-  visibility: hidden;
-}
-
-.helpIcon {
-  cursor: pointer;
-}
--- a/toolkit/themes/pinstripe/mozapps/plugins/pluginProblem.css
+++ b/toolkit/themes/pinstripe/mozapps/plugins/pluginProblem.css
@@ -95,27 +95,63 @@ html|a {
 :-moz-handler-clicktoplay .msgTapToPlay {
   display: none;
 }
 
 .submitStatus div {
   min-height: 19px; /* height of biggest line (with throbber) */
 }
 
-.msgBottomLinks {
-  padding-left: 2px;
-  padding-right: 2px;
+.submitComment {
+  width: 340px;
+  height: 70px;
+  padding: 5px;
+  border: none;
+  border-radius: 5px;
+  resize: none;
+  font-family: inherit;
+  font-size: inherit;
+}
+
+.submitURLOptInBox {
+  text-align: start;
+}
+
+.submitURLOptIn {
+  margin-left: -1px;
+}
+
+.mainBox[chromedir="rtl"] .submitURLOptIn {
+  margin-left: 0;
+  margin-right: -1px;
+}
+
+.submitButtonBox {
+  margin-top: 7px;
+}
+
+.submitButton {
+  float: right;
+}
+
+.mainBox[chromedir="rtl"] .submitButton {
+  float: left;
 }
 
 .helpIcon {
-  float: left;
   display: inline-block;
   min-width: 16px;
   min-height: 16px;
   background: url(chrome://mozapps/skin/plugins/pluginHelp-16.png) no-repeat;
+  cursor: pointer;
+  float: left;
+}
+
+.mainBox[chromedir="rtl"] .helpIcon {
+  float: right;
 }
 
 .closeIcon {
   display: block;
   position: absolute;
   width: 16px;
   height: 16px;
   top: 4px;
--- a/toolkit/themes/winstripe/mozapps/plugins/pluginProblem.css
+++ b/toolkit/themes/winstripe/mozapps/plugins/pluginProblem.css
@@ -104,27 +104,63 @@ html|a {
     display: none;
   }
 }
 
 .submitStatus div {
   min-height: 19px; /* height of biggest line (with throbber) */
 }
 
-.msgBottomLinks {
-  padding-left: 2px;
-  padding-right: 2px;
+.submitComment {
+  width: 340px;
+  height: 70px;
+  padding: 5px;
+  border: none;
+  border-radius: 5px;
+  resize: none;
+  font-family: inherit;
+  font-size: inherit;
+}
+
+.submitURLOptInBox {
+  text-align: start;
+}
+
+.submitURLOptIn {
+  margin-left: -1px;
+}
+
+.mainBox[chromedir="rtl"] .submitURLOptIn {
+  margin-left: 0;
+  margin-right: -1px;
+}
+
+.submitButtonBox {
+  margin-top: 7px;
+}
+
+.submitButton {
+  float: right;
+}
+
+.mainBox[chromedir="rtl"] .submitButton {
+  float: left;
 }
 
 .helpIcon {
-  float: left;
   display: inline-block;
   min-width: 16px;
   min-height: 16px;
   background: url(chrome://mozapps/skin/plugins/pluginHelp-16.png) no-repeat;
+  cursor: pointer;
+  float: left;
+}
+
+.mainBox[chromedir="rtl"] .helpIcon {
+  float: right;
 }
 
 .closeIcon {
   display: block;
   position: absolute;
   width: 16px;
   height: 16px;
   top: 4px;