bug 539843 - Need a mechanism for plugin crashed UI to trigger crash report submission. r=dtownsend
authorTed Mielczarek <ted.mielczarek@gmail.com>
Tue, 09 Feb 2010 17:05:31 -0800
changeset 38027 059ca2bbaa7fab05bc4520ade05294c3ea3f3b4c
parent 38026 c8a2d638f5377225280a36a8534d5bcf270484f5
child 38028 748f275c6f1f1476d6e14ba63315c4c6d09fe86a
push idunknown
push userunknown
push dateunknown
reviewersdtownsend
bugs539843
milestone1.9.3a2pre
bug 539843 - Need a mechanism for plugin crashed UI to trigger crash report submission. r=dtownsend
toolkit/crashreporter/CrashSubmit.jsm
toolkit/crashreporter/Makefile.in
toolkit/crashreporter/content/crashes.js
toolkit/crashreporter/content/oopcrashdialog.js
toolkit/crashreporter/content/oopcrashdialog.xul
toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js
toolkit/crashreporter/test/browser/crashreport.sjs
copy from toolkit/crashreporter/content/crashes.js
copy to toolkit/crashreporter/CrashSubmit.jsm
--- a/toolkit/crashreporter/content/crashes.js
+++ b/toolkit/crashreporter/CrashSubmit.jsm
@@ -1,15 +1,57 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *    Ted Mielczarek <ted.mielczarek@gmail.com> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let EXPORTED_SYMBOLS = [
+  "CrashSubmit"
+];
+
 const Cc = Components.classes;
 const Ci = Components.interfaces;
+const STATE_START = Ci.nsIWebProgressListener.STATE_START;
+const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
 
-var reportURL = null;
-var reportsDir, pendingDir;
-var strings = null;
-var myListener = null;
+let reportURL = null;
+let strings = null;
+let myListener = null;
 
 function parseKeyValuePairs(text) {
   var lines = text.split('\n');
   var data = {};
   for (let i = 0; i < lines.length; i++) {
     if (lines[i] == '')
       continue;
 
@@ -89,33 +131,40 @@ function getL10nStrings() {
     if ('CrashDetailsURL' in crstrings)
       strings['reporturl'] = crstrings.CrashDetailsURL;
   }
 }
 
 function getPendingMinidump(id) {
   let directoryService = Cc["@mozilla.org/file/directory_service;1"].
                          getService(Ci.nsIProperties);
+  let pendingDir = directoryService.get("UAppData", Ci.nsIFile);
+  pendingDir.append("Crash Reports");
+  pendingDir.append("pending");
   let dump = pendingDir.clone();
   let extra = pendingDir.clone();
   dump.append(id + ".dmp");
   extra.append(id + ".extra");
   return [dump, extra];
 }
 
 function addFormEntry(doc, form, name, value) {
   var input = doc.createElement("input");
   input.type = "hidden";
   input.name = name;
   input.value = value;
   form.appendChild(input);
 }
 
 function writeSubmittedReport(crashID, viewURL) {
-  let reportFile = reportsDir.clone();
+  let directoryService = Cc["@mozilla.org/file/directory_service;1"].
+                           getService(Ci.nsIProperties);
+  let reportFile = directoryService.get("UAppData", Ci.nsIFile);
+  reportFile.append("Crash Reports");
+  reportFile.append("submitted");
   reportFile.append(crashID + ".txt");
   var fstream = Cc["@mozilla.org/network/file-output-stream;1"].
                 createInstance(Ci.nsIFileOutputStream);
   // open, write, truncate
   fstream.init(reportFile, -1, -1, 0);
   var os = Cc["@mozilla.org/intl/converter-output-stream;1"].
            createInstance(Ci.nsIConverterOutputStream);
   os.init(fstream, "UTF-8", 0, 0x0000);
@@ -124,307 +173,194 @@ function writeSubmittedReport(crashID, v
   if (viewURL)
      data += "\n" + strings.reporturl.replace("%s", viewURL);
 
   os.writeString(data);
   os.close();
   fstream.close();
 }
 
-function submitSuccess(ret, link, dump, extra) {
-  if (!ret.CrashID)
-    return;
-  // Write out the details file to submitted/
-  writeSubmittedReport(ret.CrashID, ret.ViewURL);
-
-  // Delete from pending dir
-  try {
-    dump.remove(false);
-    extra.remove(false);
-  }
-  catch (ex) {
-    // report an error? not much the user can do here.
-  }
-
-  if (link) {
-    // reset the link to point at our new crash report. this way, if the
-    // user clicks "Back", the link will be correct.
-    let CrashID = ret.CrashID;
-    link.firstChild.textContent = CrashID;
-    link.setAttribute("id", CrashID);
-    link.removeEventListener("click", submitPendingReport, true);
-
-    if (reportURL) {
-      link.setAttribute("href", reportURL + CrashID);
-      // redirect the user to their brand new crash report
-      window.location.href = reportURL + CrashID;
-    }
-  }
-  else {
-    window.close();
-  }
-}
-
-function submitForm(iframe, dump, extra, link)
-{
-  let reportData = parseKeyValuePairsFromFile(extra);
-  let form = iframe.contentDocument.forms[0];
-  if ('ServerURL' in reportData) {
-    form.action = reportData.ServerURL;
-    delete reportData.ServerURL;
-  }
-  else {
-    return false;
-  }
-  // add the other data
-  for (let [name, value] in Iterator(reportData)) {
-    addFormEntry(iframe.contentDocument, form, name, value);
-  }
-  // tell the server not to throttle this, since it was manually submitted
-  addFormEntry(iframe.contentDocument, form, "Throttleable", "0");
-  // add the minidump
-  iframe.contentDocument.getElementById('minidump').value = dump.path;
-
-  // web progress listener
-  const STATE_START = Ci.nsIWebProgressListener.STATE_START;
-  const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
-  myListener = {
-    QueryInterface: function(aIID) {
-      if (aIID.equals(Ci.nsIWebProgressListener) ||
-          aIID.equals(Ci.nsISupportsWeakReference) ||
-          aIID.equals(Ci.nsISupports))
-        return this;
-      throw Components.results.NS_NOINTERFACE;
-    },
-
-    onStateChange: function(aWebProgress, aRequest, aFlag, aStatus) {
-      if(aFlag & STATE_STOP) {
-        iframe.docShell.removeProgressListener(myListener);
-        myListener = null;
-	if (link)
-          link.className = "";
-
-        //XXX: give some indication of failure?
-        // check general request status first
-        if (!Components.isSuccessCode(aStatus)) {
-          document.body.removeChild(iframe);
-          return 0;
-        }
-        // check HTTP status
-        if (aRequest instanceof Ci.nsIHttpChannel &&
-            aRequest.responseStatus != 200) {
-          document.body.removeChild(iframe);
-          return 0;
-        }
-
-        var ret = parseKeyValuePairs(iframe.contentDocument.documentElement.textContent);
-        document.body.removeChild(iframe);
-        submitSuccess(ret, link, dump, extra);
-      }
-      return 0;
-    },
-
-    onLocationChange: function(aProgress, aRequest, aURI) {return 0;},
-    onProgressChange: function() {return 0;},
-    onStatusChange: function() {return 0;},
-    onSecurityChange: function() {return 0;},
-  };
-  iframe.docShell.QueryInterface(Ci.nsIWebProgress);
-  iframe.docShell.addProgressListener(myListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
-  form.submit();
-  return true;
-}
-
-function createAndSubmitForm(id, link) {
-  let [dump, extra] = getPendingMinidump(id);
-  if (!dump.exists() || !extra.exists())
-    return false;
-  let iframe = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "iframe");
-  iframe.setAttribute("type", "content");
-
-  function loadHandler() {
-    if (iframe.contentWindow.location == "about:blank")
-      return;
-    iframe.removeEventListener("load", loadHandler, true);
-    submitForm(iframe, dump, extra, link);
-  }      
-
-  iframe.addEventListener("load", loadHandler, true);
-  document.body.appendChild(iframe);
-  iframe.webNavigation.loadURI("chrome://global/content/crash-submit-form.xhtml", 0, null, null, null);
-  return true;
-}
-
-function submitPendingReport(event) {
-  var link = event.target;
-  var id = link.firstChild.textContent;
-  if (createAndSubmitForm(id, link))
-    link.className = "submitting";
-  event.preventDefault();
-  return false;
+// the Submitter class represents an individual submission.
+function Submitter(id, element, submitSuccess, submitError) {
+  this.id = id;
+  this.element = element;
+  this.document = element.ownerDocument;
+  this.successCallback = submitSuccess;
+  this.errorCallback = submitError;
 }
 
-function findInsertionPoint(reports, date) {
-  if (reports.length == 0)
-    return 0;
-
-  var min = 0;
-  var max = reports.length - 1;
-  while (min < max) {
-    var mid = parseInt((min + max) / 2);
-    if (reports[mid].date < date)
-      max = mid - 1;
-    else if (reports[mid].date > date)
-      min = mid + 1;
-    else
-      return mid;
-  }
-  if (reports[min].date <= date)
-    return min;
-  return min+1;
-}
+Submitter.prototype = {
+  submitSuccess: function Submitter_submitSuccess(ret)
+  {
+    if (!ret.CrashID) {
+      this.cleanup();
+      return;
+    }
 
-function populateReportList() {
-  var prefService = Cc["@mozilla.org/preferences-service;1"].
-                    getService(Ci.nsIPrefBranch);
+    // Write out the details file to submitted/
+    writeSubmittedReport(ret.CrashID, ret.ViewURL);
 
-  try {
-    reportURL = prefService.getCharPref("breakpad.reportURL");
-    // Ignore any non http/https urls
-    if (!/^https?:/i.test(reportURL))
-      reportURL = null;
-  }
-  catch (e) { }
-  if (!reportURL) {
-    document.getElementById("clear-reports").style.display = "none";
-    document.getElementById("reportList").style.display = "none";
-    document.getElementById("noConfig").style.display = "block";
-    return;
-  }
-  var directoryService = Cc["@mozilla.org/file/directory_service;1"].
-                         getService(Ci.nsIProperties);
-
-  reportsDir = directoryService.get("UAppData", Ci.nsIFile);
-  reportsDir.append("Crash Reports");
-  reportsDir.append("submitted");
+    // Delete from pending dir
+    try {
+      this.dump.remove(false);
+      this.extra.remove(false);
+    }
+    catch (ex) {
+      // report an error? not much the user can do here.
+    }
 
-  var reports = [];
-  if (reportsDir.exists() && reportsDir.isDirectory()) {
-    var entries = reportsDir.directoryEntries;
-    while (entries.hasMoreElements()) {
-      var file = entries.getNext().QueryInterface(Ci.nsIFile);
-      var leaf = file.leafName;
-      if (leaf.substr(0, 3) == "bp-" &&
-          leaf.substr(-4) == ".txt") {
-        var entry = {
-          id: leaf.slice(0, -4),
-          date: file.lastModifiedTime,
-          pending: false
-        };
-        var pos = findInsertionPoint(reports, entry.date);
-        reports.splice(pos, 0, entry);
-      }
-    }
-  }
-
-  pendingDir = directoryService.get("UAppData", Ci.nsIFile);
-  pendingDir.append("Crash Reports");
-  pendingDir.append("pending");
+    if (this.successCallback)
+      this.successCallback(this.id, ret);
+    this.cleanup();
+  },
 
-  if (pendingDir.exists() && pendingDir.isDirectory()) {
-    var entries = pendingDir.directoryEntries;
-    while (entries.hasMoreElements()) {
-      var file = entries.getNext().QueryInterface(Ci.nsIFile);
-      var leaf = file.leafName;
-      if (leaf.substr(-4) == ".dmp") {
-        var entry = {
-          id: leaf.slice(0, -4),
-          date: file.lastModifiedTime,
-          pending: true
-        };
-        var pos = findInsertionPoint(reports, entry.date);
-        reports.splice(pos, 0, entry);
-      }
-    }
-  }
+  cleanup: function Submitter_cleanup() {
+    // drop some references just to be nice
+    this.element = null;
+    this.document = null;
+    this.successCallback = null;
+    this.errorCallback = null;
+    this.iframe = null;
+    this.dump = null;
+    this.extra = null;
+    // remove this object from the list of active submissions
+    let idx = CrashSubmit._activeSubmissions.indexOf(this);
+    if (idx != -1)
+      CrashSubmit._activeSubmissions.splice(idx, 1);
+  },
 
-  if (reports.length == 0) {
-    document.getElementById("clear-reports").style.display = "none";
-    document.getElementById("reportList").style.display = "none";
-    document.getElementById("noReports").style.display = "block";
-    return;
-  }
-
-  var formatter = Cc["@mozilla.org/intl/scriptabledateformat;1"].
-                  createInstance(Ci.nsIScriptableDateFormat);
-  var body = document.getElementById("tbody");
-  var ios = Cc["@mozilla.org/network/io-service;1"].
-            getService(Ci.nsIIOService);
-  var reportURI = ios.newURI(reportURL, null, null);
-  // resolving this URI relative to /report/index
-  var aboutThrottling = ios.newURI("../../about/throttling", null, reportURI);
-
-  for (var i = 0; i < reports.length; i++) {
-    var row = document.createElement("tr");
-    var cell = document.createElement("td");
-    row.appendChild(cell);
-    var link = document.createElement("a");
-    if (reports[i].pending) {
-      link.setAttribute("href", aboutThrottling.spec);
-      link.addEventListener("click", submitPendingReport, true);
+  submitForm: function Submitter_submitForm()
+  {
+    let reportData = parseKeyValuePairsFromFile(this.extra);
+    let form = this.iframe.contentDocument.forms[0];
+    if ('ServerURL' in reportData) {
+      form.action = reportData.ServerURL;
+      delete reportData.ServerURL;
     }
     else {
-      link.setAttribute("href", reportURL + reports[i].id);
+      return false;
+    }
+    // add the other data
+    for (let [name, value] in Iterator(reportData)) {
+      addFormEntry(this.iframe.contentDocument, form, name, value);
     }
-    link.setAttribute("id", reports[i].id);
-    link.appendChild(document.createTextNode(reports[i].id));
-    cell.appendChild(link);
+    // tell the server not to throttle this, since it was manually submitted
+    addFormEntry(this.iframe.contentDocument, form, "Throttleable", "0");
+    // add the minidump
+    this.iframe.contentDocument.getElementById('minidump').value
+      = this.dump.path;
+    this.iframe.docShell.QueryInterface(Ci.nsIWebProgress);
+    this.iframe.docShell.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+    form.submit();
+    return true;
+  },
+
+  // web progress listener
+  QueryInterface: function(aIID)
+  {
+    if (aIID.equals(Ci.nsIWebProgressListener) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+    throw Components.results.NS_NOINTERFACE;
+  },
+
+  onStateChange: function(aWebProgress, aRequest, aFlag, aStatus)
+  {
+    if(aFlag & STATE_STOP) {
+      this.iframe.docShell.removeProgressListener(this);
 
-    var date = new Date(reports[i].date);
-    cell = document.createElement("td");
-    var datestr = formatter.FormatDate("",
-                                       Ci.nsIScriptableDateFormat.dateFormatShort,
-                                       date.getFullYear(),
-                                       date.getMonth() + 1,
-                                       date.getDate());
-    cell.appendChild(document.createTextNode(datestr));
-    row.appendChild(cell);
-    cell = document.createElement("td");
-    var timestr = formatter.FormatTime("",
-                                       Ci.nsIScriptableDateFormat.timeFormatNoSeconds,
-                                       date.getHours(),
-                                       date.getMinutes(),
-                                       date.getSeconds());
-    cell.appendChild(document.createTextNode(timestr));
-    row.appendChild(cell);
-    body.appendChild(row);
-  }
-}
+      // check general request status first
+      if (!Components.isSuccessCode(aStatus)) {
+        this.element.removeChild(this.iframe);
+        if (this.errorCallback) {
+          this.errorCallback(this.id);
+          this.cleanup();
+        }
+        return 0;
+      }
+      // check HTTP status
+      if (aRequest instanceof Ci.nsIHttpChannel &&
+          aRequest.responseStatus != 200) {
+        this.element.removeChild(this.iframe);
+        if (this.errorCallback) {
+          this.errorCallback(this.id);
+          this.cleanup();
+        }
+        return 0;
+      }
+
+      var ret = parseKeyValuePairs(this.iframe.contentDocument.documentElement.textContent);
+      this.element.removeChild(this.iframe);
+      this.submitSuccess(ret);
+    }
+    return 0;
+  },
+
+  onLocationChange: function(aProgress, aRequest, aURI) {return 0;},
+  onProgressChange: function() {return 0;},
+  onStatusChange: function() {return 0;},
+  onSecurityChange: function() {return 0;},
 
-function clearReports() {
-  var bundles = Cc["@mozilla.org/intl/stringbundle;1"].
-                getService(Ci.nsIStringBundleService);
-  var bundle = bundles.createBundle("chrome://global/locale/crashes.properties");
-  var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
-                getService(Ci.nsIPromptService);
-  if (!prompts.confirm(window,
-                       bundle.GetStringFromName("deleteconfirm.title"),
-                       bundle.GetStringFromName("deleteconfirm.description")))
-    return;
+  submit: function Submitter_submit()
+  {
+    let [dump, extra] = getPendingMinidump(this.id);
+    if (!dump.exists() || !extra.exists())
+      return false;
+    this.dump = dump;
+    this.extra = extra;
+    let iframe = this.document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "iframe");
+    iframe.setAttribute("type", "content");
+
+    let self = this;
+    function loadHandler() {
+      if (iframe.contentWindow.location == "about:blank")
+        return;
+      iframe.removeEventListener("load", loadHandler, true);
+      self.submitForm();
+    }
+
+    iframe.addEventListener("load", loadHandler, true);
+    this.element.appendChild(iframe);
+    this.iframe = iframe;
+    iframe.webNavigation.loadURI("chrome://global/content/crash-submit-form.xhtml", 0, null, null, null);
+    return true;
+  }
+};
 
-  var entries = reportsDir.directoryEntries;
-  while (entries.hasMoreElements()) {
-    var file = entries.getNext().QueryInterface(Ci.nsIFile);
-    var leaf = file.leafName;
-    if (leaf.substr(0, 3) == "bp-" &&
-        leaf.substr(-4) == ".txt") {
-      file.remove(false);
-    }
-  }
-  document.getElementById("clear-reports").style.display = "none";
-  document.getElementById("reportList").style.display = "none";
-  document.getElementById("noReports").style.display = "block";
-}
+//===================================
+// External API goes here
+let CrashSubmit = {
+  /**
+   * Submit the crash report named id.dmp from the "pending" directory.
+   *
+   * @param id
+   *        Filename (minus .dmp extension) of the minidump to submit.
+   * @param element
+   *        A DOM element to which an iframe can be appended as a child,
+   *        used for form submission.
+   * @param submitSuccess
+   *        A function that will be called if the report is submitted
+   *        successfully with two parameters: the id that was passed
+   *        to this function, and an object containing the key/value
+   *        data returned from the server in its properties.
+   * @param submitError
+   *        A function that will be called with one parameter if the
+   *        report fails to submit: the id that was passed to this
+   *        function.
+   *
+   * @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, element, submitSuccess, submitError)
+  {
+    let submitter = new Submitter(id, element, submitSuccess, submitError);
+    CrashSubmit._activeSubmissions.push(submitter);
+    return submitter.submit();
+  },
 
-function init() {
-  getL10nStrings();
-  populateReportList();
-}
+  // List of currently active submit objects
+  _activeSubmissions: []
+};
+
+// Run this when first loaded
+getL10nStrings();
\ No newline at end of file
--- a/toolkit/crashreporter/Makefile.in
+++ b/toolkit/crashreporter/Makefile.in
@@ -106,13 +106,17 @@ EXPORTS = \
 	$(NULL)
 
 CPPSRCS = \
 	nsExceptionHandler.cpp \
 	$(NULL)
 
 FORCE_STATIC_LIB = 1
 
+EXTRA_JS_MODULES = \
+  CrashSubmit.jsm \
+  $(NULL)
+
 ifdef ENABLE_TESTS
 TOOL_DIRS = test
 endif
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/crashreporter/content/crashes.js
+++ b/toolkit/crashreporter/content/crashes.js
@@ -1,267 +1,87 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *    Dave Townsend <dtownsend@oxymoronical> (original author)
+ *    Ted Mielczarek <ted.mielczarek@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
-var reportURL = null;
 var reportsDir, pendingDir;
-var strings = null;
-var myListener = null;
-
-function parseKeyValuePairs(text) {
-  var lines = text.split('\n');
-  var data = {};
-  for (let i = 0; i < lines.length; i++) {
-    if (lines[i] == '')
-      continue;
-
-    // can't just .split() because the value might contain = characters
-    let eq = lines[i].indexOf('=');
-    if (eq != -1) {
-      let [key, value] = [lines[i].substring(0, eq),
-                          lines[i].substring(eq + 1)];
-      if (key && value)
-        data[key] = value.replace("\\n", "\n", "g").replace("\\\\", "\\", "g");
-    }
-  }
-  return data;
-}
 
-function parseKeyValuePairsFromFile(file) {
-  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
-                createInstance(Ci.nsIFileInputStream);
-  fstream.init(file, -1, 0, 0);
-  var is = Cc["@mozilla.org/intl/converter-input-stream;1"].
-           createInstance(Ci.nsIConverterInputStream);
-  is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
-  var str = {};
-  var contents = '';
-  while (is.readString(4096, str) != 0) {
-    contents += str.value;
-  }
-  is.close();
-  fstream.close();
-  return parseKeyValuePairs(contents);
-}
-
-function parseINIStrings(file) {
-  var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
-                getService(Ci.nsIINIParserFactory);
-  var parser = factory.createINIParser(file);
-  var obj = {};
-  var en = parser.getKeys("Strings");
-  while (en.hasMore()) {
-    var key = en.getNext();
-    obj[key] = parser.getString("Strings", key);
-  }
-  return obj;
-}
+Components.utils.import("resource://gre/modules/CrashSubmit.jsm");
 
-// Since we're basically re-implementing part of the crashreporter
-// client here, we'll just steal the strings we need from crashreporter.ini
-function getL10nStrings() {
-  let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
-               getService(Ci.nsIProperties);
-  let path = dirSvc.get("GreD", Ci.nsIFile);
-  path.append("crashreporter.ini");
-  if (!path.exists()) {
-    // see if we're on a mac
-    path = path.parent;
-    path.append("crashreporter.app");
-    path.append("Contents");
-    path.append("MacOS");
-    path.append("crashreporter.ini");
-    if (!path.exists()) {
-      // very bad, but I don't know how to recover
-      return;
-    }
-  }
-  let crstrings = parseINIStrings(path);
-  strings = {
-    'crashid': crstrings.CrashID,
-    'reporturl': crstrings.CrashDetailsURL
-  };
-
-  path = dirSvc.get("XCurProcD", Ci.nsIFile);
-  path.append("crashreporter-override.ini");
-  if (path.exists()) {
-    crstrings = parseINIStrings(path);
-    if ('CrashID' in crstrings)
-      strings['crashid'] = crstrings.CrashID;
-    if ('CrashDetailsURL' in crstrings)
-      strings['reporturl'] = crstrings.CrashDetailsURL;
-  }
-}
-
-function getPendingMinidump(id) {
-  let directoryService = Cc["@mozilla.org/file/directory_service;1"].
-                         getService(Ci.nsIProperties);
-  let dump = pendingDir.clone();
-  let extra = pendingDir.clone();
-  dump.append(id + ".dmp");
-  extra.append(id + ".extra");
-  return [dump, extra];
-}
-
-function addFormEntry(doc, form, name, value) {
-  var input = doc.createElement("input");
-  input.type = "hidden";
-  input.name = name;
-  input.value = value;
-  form.appendChild(input);
-}
-
-function writeSubmittedReport(crashID, viewURL) {
-  let reportFile = reportsDir.clone();
-  reportFile.append(crashID + ".txt");
-  var fstream = Cc["@mozilla.org/network/file-output-stream;1"].
-                createInstance(Ci.nsIFileOutputStream);
-  // open, write, truncate
-  fstream.init(reportFile, -1, -1, 0);
-  var os = Cc["@mozilla.org/intl/converter-output-stream;1"].
-           createInstance(Ci.nsIConverterOutputStream);
-  os.init(fstream, "UTF-8", 0, 0x0000);
-
-  var data = strings.crashid.replace("%s", crashID);
-  if (viewURL)
-     data += "\n" + strings.reporturl.replace("%s", viewURL);
-
-  os.writeString(data);
-  os.close();
-  fstream.close();
-}
-
-function submitSuccess(ret, link, dump, extra) {
-  if (!ret.CrashID)
-    return;
-  // Write out the details file to submitted/
-  writeSubmittedReport(ret.CrashID, ret.ViewURL);
-
-  // Delete from pending dir
-  try {
-    dump.remove(false);
-    extra.remove(false);
-  }
-  catch (ex) {
-    // report an error? not much the user can do here.
-  }
-
+function submitSuccess(dumpid, ret) {
+  let link = document.getElementById(dumpid);
   if (link) {
+    link.className = "";
     // reset the link to point at our new crash report. this way, if the
     // user clicks "Back", the link will be correct.
     let CrashID = ret.CrashID;
     link.firstChild.textContent = CrashID;
     link.setAttribute("id", CrashID);
     link.removeEventListener("click", submitPendingReport, true);
 
     if (reportURL) {
       link.setAttribute("href", reportURL + CrashID);
       // redirect the user to their brand new crash report
       window.location.href = reportURL + CrashID;
     }
   }
-  else {
-    window.close();
-  }
 }
 
-function submitForm(iframe, dump, extra, link)
-{
-  let reportData = parseKeyValuePairsFromFile(extra);
-  let form = iframe.contentDocument.forms[0];
-  if ('ServerURL' in reportData) {
-    form.action = reportData.ServerURL;
-    delete reportData.ServerURL;
-  }
-  else {
-    return false;
-  }
-  // add the other data
-  for (let [name, value] in Iterator(reportData)) {
-    addFormEntry(iframe.contentDocument, form, name, value);
-  }
-  // tell the server not to throttle this, since it was manually submitted
-  addFormEntry(iframe.contentDocument, form, "Throttleable", "0");
-  // add the minidump
-  iframe.contentDocument.getElementById('minidump').value = dump.path;
-
-  // web progress listener
-  const STATE_START = Ci.nsIWebProgressListener.STATE_START;
-  const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
-  myListener = {
-    QueryInterface: function(aIID) {
-      if (aIID.equals(Ci.nsIWebProgressListener) ||
-          aIID.equals(Ci.nsISupportsWeakReference) ||
-          aIID.equals(Ci.nsISupports))
-        return this;
-      throw Components.results.NS_NOINTERFACE;
-    },
-
-    onStateChange: function(aWebProgress, aRequest, aFlag, aStatus) {
-      if(aFlag & STATE_STOP) {
-        iframe.docShell.removeProgressListener(myListener);
-        myListener = null;
-	if (link)
-          link.className = "";
-
-        //XXX: give some indication of failure?
-        // check general request status first
-        if (!Components.isSuccessCode(aStatus)) {
-          document.body.removeChild(iframe);
-          return 0;
-        }
-        // check HTTP status
-        if (aRequest instanceof Ci.nsIHttpChannel &&
-            aRequest.responseStatus != 200) {
-          document.body.removeChild(iframe);
-          return 0;
-        }
-
-        var ret = parseKeyValuePairs(iframe.contentDocument.documentElement.textContent);
-        document.body.removeChild(iframe);
-        submitSuccess(ret, link, dump, extra);
-      }
-      return 0;
-    },
-
-    onLocationChange: function(aProgress, aRequest, aURI) {return 0;},
-    onProgressChange: function() {return 0;},
-    onStatusChange: function() {return 0;},
-    onSecurityChange: function() {return 0;},
-  };
-  iframe.docShell.QueryInterface(Ci.nsIWebProgress);
-  iframe.docShell.addProgressListener(myListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
-  form.submit();
-  return true;
-}
-
-function createAndSubmitForm(id, link) {
-  let [dump, extra] = getPendingMinidump(id);
-  if (!dump.exists() || !extra.exists())
-    return false;
-  let iframe = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "iframe");
-  iframe.setAttribute("type", "content");
-
-  function loadHandler() {
-    if (iframe.contentWindow.location == "about:blank")
-      return;
-    iframe.removeEventListener("load", loadHandler, true);
-    submitForm(iframe, dump, extra, link);
-  }      
-
-  iframe.addEventListener("load", loadHandler, true);
-  document.body.appendChild(iframe);
-  iframe.webNavigation.loadURI("chrome://global/content/crash-submit-form.xhtml", 0, null, null, null);
-  return true;
+function submitError(dumpid) {
+  //XXX: do something more useful here
+  let link = document.getElementById(dumpid);
+  if (link)
+    link.className = "";
+  // dispatch an event, useful for testing
+  let event = document.createEvent("Events");
+  event.initEvent("CrashSubmitFailed", true, false);
+  document.dispatchEvent(event);
 }
 
 function submitPendingReport(event) {
   var link = event.target;
   var id = link.firstChild.textContent;
-  if (createAndSubmitForm(id, link))
+  if (CrashSubmit.submit(id, document.body, submitSuccess, submitError))
     link.className = "submitting";
   event.preventDefault();
   return false;
 }
 
 function findInsertionPoint(reports, date) {
   if (reports.length == 0)
     return 0;
@@ -420,11 +240,10 @@ function clearReports() {
     }
   }
   document.getElementById("clear-reports").style.display = "none";
   document.getElementById("reportList").style.display = "none";
   document.getElementById("noReports").style.display = "block";
 }
 
 function init() {
-  getL10nStrings();
   populateReportList();
 }
--- a/toolkit/crashreporter/content/oopcrashdialog.js
+++ b/toolkit/crashreporter/content/oopcrashdialog.js
@@ -1,19 +1,19 @@
 // This code is TEMPORARY for submitting crashes via an ugly popup dialog:
 // bug 525849 tracks the real implementation.
 
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/CrashSubmit.jsm");
+
 var id;
 
 function collectData() {
-  // HACK: crashes.js uses document.body, so we just alias it
-  document.body = document.getElementById('iframe-holder');
-
-  getL10nStrings();
-
   let directoryService = Cc["@mozilla.org/file/directory_service;1"].
     getService(Ci.nsIProperties);
   pendingDir = directoryService.get("UAppData", Ci.nsIFile);
   pendingDir.append("Crash Reports");
   pendingDir.append("pending");
   if (!pendingDir.exists())
     pendingDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0770);
 
@@ -26,16 +26,23 @@ function collectData() {
   let dumpFile = window.arguments[0].QueryInterface(Ci.nsIFile);
   let extraFile = dumpFile.clone();
   id = dumpFile.leafName.replace(/.dmp$/, "");
   extraFile.leafName = id + ".extra";
   dumpFile.moveTo(pendingDir, "");
   extraFile.moveTo(pendingDir, "");
 }
 
+function submitDone()
+{
+  // we don't currently distinguish between success or failure here
+  window.close();
+}
+
 function onSubmit()
 {
   document.documentElement.getButton('accept').disabled = true;
   document.documentElement.getButton('accept').label = 'Sending';
   document.getElementById('throbber').src = 'chrome://global/skin/icons/loading_16.png';
-  createAndSubmitForm(id, null);
+  CrashSubmit.submit(id, document.getElementById('iframe-holder'),
+                     submitDone, submitDone);
   return false;
 }
--- a/toolkit/crashreporter/content/oopcrashdialog.xul
+++ b/toolkit/crashreporter/content/oopcrashdialog.xul
@@ -12,17 +12,16 @@
   <style xmlns="http://www.w3.org/1999/xhtml" type="text/css">
     #iframe-holder {
       visibility: hidden;
       height: 1px;
       overflow: hidden;
     }
   </style>
 
-  <script type="application/javascript;version=1.8" src="chrome://global/content/crashes.js"/>
   <script type="application/javascript;version=1.8" src="chrome://global/content/oopcrashdialog.js"/>
 
   <dialogheader title="A Plugin Crashed" />
 
   <hbox align="center">
     <description>A plugin crashed while Firefox was running. Please choose to send a crash report
     to Mozilla. Reloading should cause your plugin to restart.</description>
     <image width="16" height="16" id="throbber" />
--- a/toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js
+++ b/toolkit/crashreporter/test/browser/browser_aboutCrashesResubmit.js
@@ -48,17 +48,16 @@ function check_crash_list(tab, crashes) 
  * Additionally, click "back" and verify that the link now points to our new
  */
 function check_submit_pending(tab, crashes) {
   let browser = gBrowser.getBrowserForTab(tab);
   let SubmittedCrash = null;
   let CrashID = null;
   let CrashURL = null;
   function csp_onload() {
-    dump('csp_onload, location = ' + browser.contentWindow.location + '\n');
     if (browser.contentWindow.location != 'about:crashes') {
       browser.removeEventListener("load", csp_onload, true);
       // loaded the crash report page
       ok(true, 'got submission onload');
       // grab the Crash ID here to verify later
       CrashID = browser.contentWindow.location.search.split("=")[1];
       CrashURL = browser.contentWindow.location.toString();
       // check the JSON content vs. what we submitted
@@ -85,16 +84,22 @@ function check_submit_pending(tab, crash
       }
       executeSoon(function() {
                     browser.addEventListener("pageshow", csp_pageshow, true);
                     // now navigate back
                     browser.goBack();
                   });
     }
   }
+  function csp_fail() {
+    browser.removeEventListener("CrashSubmitFailed", csp_fail, true);
+    ok(false, "failed to submit crash report!");
+    cleanup_and_finish();
+  }
+  browser.addEventListener("CrashSubmitFailed", csp_fail, true);
   browser.addEventListener("load", csp_onload, true);
   function csp_pageshow() {
     browser.removeEventListener("pageshow", csp_pageshow, true);
     executeSoon(function () {
                   is(browser.contentWindow.location, "about:crashes", "navigated back successfully");
                   let link = browser.contentDocument.getElementById(CrashID);
                   isnot(link, null, "crash report link changed correctly");
                   if (link)
@@ -102,23 +107,21 @@ function check_submit_pending(tab, crash
                   cleanup_and_finish();
                 });
   }
 
   // try submitting the pending report
   for each(let crash in crashes) {
     if (crash.pending) {
       SubmittedCrash = crash;
-      dump('check_submit_pending: trying to submit crash ' + crash.id + '\n');
       break;
     }
   }
   EventUtils.sendMouseEvent({type:'click'}, SubmittedCrash.id,
                             browser.contentWindow);
-  dump('check_submit_pending: sent mouse event to ' + SubmittedCrash.id + '\n');
 }
 
 function test() {
   waitForExplicitFinish();
   let appD = make_fake_appdir();
   let crD = appD.clone();
   crD.append("Crash Reports");
   let crashes = add_fake_crashes(crD, 1);
--- a/toolkit/crashreporter/test/browser/crashreport.sjs
+++ b/toolkit/crashreporter/test/browser/crashreport.sjs
@@ -140,32 +140,33 @@ function handleRequest(request, response
         response.setHeader("Content-Type", "text/plain", false);
         response.write(data);
       }
     }
   }
   else if (request.method == "POST") {
     let formData = parseMultipartForm(request);
 
-    if ('upload_file_minidump' in formData) {
+    if (formData && 'upload_file_minidump' in formData) {
       response.setHeader("Content-Type", "text/plain", false);
 
       let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
         .getService(Ci.nsIUUIDGenerator);
       let uuid = uuidGenerator.generateUUID().toString();
       // ditch the {}, add bp- prefix
       uuid = 'bp-' + uuid.substring(1,uuid.length-2);
 
       let d = JSON.stringify(formData);
       //dump('saving crash report ' + uuid + ': ' + d + '\n');
       setState(uuid, d);
 
       response.write("CrashID=" + uuid + "\n");
     }
     else {
+      dump('*** crashreport.sjs: Malformed request?\n');
       response.setStatusLine(request.httpVersion, 400, "Bad Request");
       response.write("Missing minidump file");
     }
   }
   else {
     response.setStatusLine(request.httpVersion, 405, "Method not allowed");
     response.write("Can't handle HTTP method " + request.method);
   }