/* ***** 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 (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; const SUCCESS = "success"; const FAILED = "failed"; const SUBMITTING = "submitting"; 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; // 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; } // 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 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 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"); if (!reportFile.exists()) reportFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); 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(); } // the Submitter class represents an individual submission. function Submitter(id, element, submitSuccess, submitError, noThrottle) { this.id = id; this.element = element; this.document = element.ownerDocument; this.successCallback = submitSuccess; this.errorCallback = submitError; this.noThrottle = noThrottle; } Submitter.prototype = { submitSuccess: function Submitter_submitSuccess(ret) { if (!ret.CrashID) { this.notifyStatus(FAILED); this.cleanup(); return; } // Write out the details file to submitted/ writeSubmittedReport(ret.CrashID, ret.ViewURL); // 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. } this.notifyStatus(SUCCESS, ret); this.cleanup(); }, 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); }, 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 { return false; } // add the other data for (let [name, value] in Iterator(reportData)) { addFormEntry(this.iframe.contentDocument, form, name, value); } if (this.noThrottle) { // 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.QueryInterface(Ci.nsIWebProgress); this.iframe.docShell.removeProgressListener(this); // check general request status first if (!Components.isSuccessCode(aStatus)) { this.element.removeChild(this.iframe); this.notifyStatus(FAILED); this.cleanup(); return 0; } // check HTTP status if (aRequest instanceof Ci.nsIHttpChannel && aRequest.responseStatus != 200) { this.element.removeChild(this.iframe); this.notifyStatus(FAILED); 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;}, notifyStatus: function Submitter_notify(status, ret) { 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); } Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService) .notifyObservers(propBag, "crash-report-status", status); switch (status) { case SUCCESS: if (this.successCallback) this.successCallback(this.id, ret); break; case FAILED: if (this.errorCallback) this.errorCallback(this.id); break; default: // no callbacks invoked. } }, submit: function Submitter_submit() { let [dump, extra] = getPendingMinidump(this.id); if (!dump.exists() || !extra.exists()) { this.notifyStatus(FAILED); this.cleanup(); return false; } this.notifyStatus(SUBMITTING); 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"); iframe.style.width = 0; iframe.style.minWidth = 0; let self = this; function loadHandler() { if (iframe.contentWindow.location == "about:blank") return; iframe.removeEventListener("load", loadHandler, true); if (!self.submitForm()) { this.notifyStatus(FAILED); self.cleanup(); } } 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; } }; //=================================== // 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. * @param 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. * * @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, noThrottle) { let submitter = new Submitter(id, element, submitSuccess, submitError, noThrottle); CrashSubmit._activeSubmissions.push(submitter); return submitter.submit(); }, // List of currently active submit objects _activeSubmissions: [] }; // Run this when first loaded getL10nStrings();