/* 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/. */ "use strict"; var EXPORTED_SYMBOLS = ["TabCrashHandler", "UnsubmittedCrashHandler"]; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", clearTimeout: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", }); XPCOMUtils.defineLazyModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", CrashSubmit: "resource://gre/modules/CrashSubmit.jsm", }); // We don't process crash reports older than 28 days, so don't bother // submitting them const PENDING_CRASH_REPORT_DAYS = 28; const DAY = 24 * 60 * 60 * 1000; // milliseconds const DAYS_TO_SUPPRESS = 30; const MAX_UNSEEN_CRASHED_CHILD_IDS = 20; const MAX_UNSEEN_CRASHED_SUBFRAME_IDS = 10; // Time after which we will begin scanning for unsubmitted crash reports const CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS = 60 * 10000; // 10 minutes // This is SIGUSR1 and indicates a user-invoked crash const EXIT_CODE_CONTENT_CRASHED = 245; const TABCRASHED_ICON_URI = "chrome://browser/skin/tab-crashed.svg"; const SUBFRAMECRASH_LEARNMORE_URI = "https://support.mozilla.org/kb/firefox-crashes-troubleshoot-prevent-and-get-help"; /** * BrowserWeakMap is exactly like a WeakMap, but expects * objects only. * * Under the hood, BrowserWeakMap keys the map off of the * permanentKey. If, however, the browser has never gotten a permanentKey, * it falls back to keying on the element itself. */ class BrowserWeakMap extends WeakMap { get(browser) { if (browser.permanentKey) { return super.get(browser.permanentKey); } return super.get(browser); } set(browser, value) { if (browser.permanentKey) { return super.set(browser.permanentKey, value); } return super.set(browser, value); } delete(browser) { if (browser.permanentKey) { return super.delete(browser.permanentKey); } return super.delete(browser); } } var TabCrashHandler = { _crashedTabCount: 0, childMap: new Map(), browserMap: new BrowserWeakMap(), notificationsMap: new Map(), unseenCrashedChildIDs: [], pendingSubFrameCrashes: new Map(), pendingSubFrameCrashesIDs: [], crashedBrowserQueues: new Map(), restartRequiredBrowsers: new WeakSet(), testBuildIDMismatch: false, get prefs() { delete this.prefs; return (this.prefs = Services.prefs.getBranch( "browser.tabs.crashReporting." )); }, init() { if (this.initialized) { return; } this.initialized = true; Services.obs.addObserver(this, "ipc:content-shutdown"); Services.obs.addObserver(this, "oop-frameloader-crashed"); }, observe(aSubject, aTopic, aData) { switch (aTopic) { case "ipc:content-shutdown": { aSubject.QueryInterface(Ci.nsIPropertyBag2); if (!aSubject.get("abnormal")) { return; } let childID = aSubject.get("childID"); let dumpID = aSubject.get("dumpID"); // Get and remove the subframe crash info first. let subframeCrashItem = this.getAndRemoveSubframeCrash(childID); if (!dumpID) { Services.telemetry .getHistogramById("FX_CONTENT_CRASH_DUMP_UNAVAILABLE") .add(1); } else if (AppConstants.MOZ_CRASHREPORTER) { this.childMap.set(childID, dumpID); // If this is a subframe crash, show the crash notification. Only // show subframe notifications when there is a minidump available. if (subframeCrashItem) { let browsers = ChromeUtils.nondeterministicGetWeakMapKeys(subframeCrashItem) || []; for (let browserItem of browsers) { let browser = subframeCrashItem.get(browserItem); if (browser.isConnected && !browser.ownerGlobal.closed) { this.showSubFrameNotification(browser, childID, dumpID); } } } } if (!this.flushCrashedBrowserQueue(childID)) { this.unseenCrashedChildIDs.push(childID); // The elements in unseenCrashedChildIDs will only be removed if // the tab crash page is shown. However, ipc:content-shutdown might // be fired for processes for which we'll never show the tab crash // page - for example, the thumbnailing process. Another case to // consider is if the user is configured to submit backlogged crash // reports automatically, and a background tab crashes. In that case, // we will never show the tab crash page, and never remove the element // from the list. // // Instead of trying to account for all of those cases, we prevent // this list from getting too large by putting a reasonable upper // limit on how many childIDs we track. It's unlikely that this // array would ever get so large as to be unwieldy (that'd be a lot // or crashes!), but a leak is a leak. if ( this.unseenCrashedChildIDs.length > MAX_UNSEEN_CRASHED_CHILD_IDS ) { this.unseenCrashedChildIDs.shift(); } } // check for environment affecting crash reporting let shutdown = Services.env.exists("MOZ_CRASHREPORTER_SHUTDOWN"); if (shutdown) { dump( "A content process crashed and MOZ_CRASHREPORTER_SHUTDOWN is " + "set, shutting down\n" ); Services.startup.quit( Ci.nsIAppStartup.eForceQuit, EXIT_CODE_CONTENT_CRASHED ); } break; } case "oop-frameloader-crashed": { let browser = aSubject.ownerElement; if (!browser) { return; } this.browserMap.set(browser, aSubject.childID); break; } } }, /** * This should be called once a content process has finished * shutting down abnormally. Any tabbrowser browsers that were * selected at the time of the crash will then be sent to * the crashed tab page. * * @param childID (int) * The childID of the content process that just crashed. * @returns boolean * True if one or more browsers were sent to the tab crashed * page. */ flushCrashedBrowserQueue(childID) { let browserQueue = this.crashedBrowserQueues.get(childID); if (!browserQueue) { return false; } this.crashedBrowserQueues.delete(childID); let sentBrowser = false; for (let weakBrowser of browserQueue) { let browser = weakBrowser.get(); if (browser) { if ( this.restartRequiredBrowsers.has(browser) || this.testBuildIDMismatch ) { this.sendToRestartRequiredPage(browser); } else { this.sendToTabCrashedPage(browser); } sentBrowser = true; } } return sentBrowser; }, /** * Called by a tabbrowser when it notices that its selected browser * has crashed. This will queue the browser to show the tab crash * page once the content process has finished tearing down. * * @param browser () * The selected browser that just crashed. * @param restartRequired (bool) * Whether or not a browser restart is required to recover. */ onSelectedBrowserCrash(browser, restartRequired) { if (!browser.isRemoteBrowser) { console.error("Selected crashed browser is not remote."); return; } if (!browser.frameLoader) { console.error("Selected crashed browser has no frameloader."); return; } let childID = browser.frameLoader.childID; let browserQueue = this.crashedBrowserQueues.get(childID); if (!browserQueue) { browserQueue = []; this.crashedBrowserQueues.set(childID, browserQueue); } // It's probably unnecessary to store this browser as a // weak reference, since the content process should complete // its teardown in the same tick of the event loop, and then // this queue will be flushed. The weak reference is to avoid // leaking browsers in case anything goes wrong during this // teardown process. browserQueue.push(Cu.getWeakReference(browser)); if (restartRequired) { this.restartRequiredBrowsers.add(browser); } // In the event that the content process failed to launch, then // the childID will be 0. In that case, we will never receive // a dumpID nor an ipc:content-shutdown observer notification, // so we should flush the queue for childID 0 immediately. if (childID == 0) { this.flushCrashedBrowserQueue(0); } }, /** * Called by a tabbrowser when it notices that a background browser * has crashed. This will flip its remoteness to non-remote, and attempt * to revive the crashed tab so that upon selection the tab either shows * an error page, or automatically restores. * * @param browser () * The background browser that just crashed. * @param restartRequired (bool) * Whether or not a browser restart is required to recover. */ onBackgroundBrowserCrash(browser, restartRequired) { if (restartRequired) { this.restartRequiredBrowsers.add(browser); } let gBrowser = browser.getTabBrowser(); let tab = gBrowser.getTabForBrowser(browser); gBrowser.updateBrowserRemoteness(browser, { remoteType: lazy.E10SUtils.NOT_REMOTE, }); lazy.SessionStore.reviveCrashedTab(tab); }, /** * Called when a subframe crashes. If the dump is available, shows a subframe * crashed notification, otherwise waits for one to be available. * * @param browser () * The browser containing the frame that just crashed. * @param childId * The id of the process that just crashed. */ async onSubFrameCrash(browser, childID) { if (!AppConstants.MOZ_CRASHREPORTER) { return; } // If a crash dump is available, use it. Otherwise, add the child id to the pending // subframe crashes list, and wait for the crash "ipc:content-shutdown" notification // to get the minidump. If it never arrives, don't show the notification. let dumpID = this.childMap.get(childID); if (dumpID) { this.showSubFrameNotification(browser, childID, dumpID); } else { let item = this.pendingSubFrameCrashes.get(childID); if (!item) { item = new BrowserWeakMap(); this.pendingSubFrameCrashes.set(childID, item); // Add the childID to an array that only has room for MAX_UNSEEN_CRASHED_SUBFRAME_IDS // items. If there is no more room, pop the oldest off and remove it. This technique // is used instead of a timeout. if ( this.pendingSubFrameCrashesIDs.length >= MAX_UNSEEN_CRASHED_SUBFRAME_IDS ) { let idToDelete = this.pendingSubFrameCrashesIDs.shift(); this.pendingSubFrameCrashes.delete(idToDelete); } this.pendingSubFrameCrashesIDs.push(childID); } item.set(browser, browser); } }, /** * Given a childID, retrieve the subframe crash info for it * from the pendingSubFrameCrashes map. The data is removed * from the map and returned. * * @param childID number * childID of the content that crashed. * @returns subframe crash info added by previous call to onSubFrameCrash. */ getAndRemoveSubframeCrash(childID) { let item = this.pendingSubFrameCrashes.get(childID); if (item) { this.pendingSubFrameCrashes.delete(childID); let idx = this.pendingSubFrameCrashesIDs.indexOf(childID); if (idx >= 0) { this.pendingSubFrameCrashesIDs.splice(idx, 1); } } return item; }, /** * Called to indicate that a subframe within a browser has crashed. A notification * bar will be shown. * * @param browser () * The browser containing the frame that just crashed. * @param childId * The id of the process that just crashed. * @param dumpID * Minidump id of the crash. */ showSubFrameNotification(browser, childID, dumpID) { let gBrowser = browser.getTabBrowser(); let notificationBox = gBrowser.getNotificationBox(browser); const value = "subframe-crashed"; let notification = notificationBox.getNotificationWithValue(value); if (notification) { // Don't show multiple notifications for a browser. return; } let closeAllNotifications = () => { // Close all other notifications on other tabs that might // be open for the same crashed process. let existingItem = this.notificationsMap.get(childID); if (existingItem) { for (let notif of existingItem.slice()) { notif.close(); } } }; gBrowser.ownerGlobal.MozXULElement.insertFTLIfNeeded( "browser/contentCrash.ftl" ); let buttons = [ { "l10n-id": "crashed-subframe-learnmore-link", popup: null, link: SUBFRAMECRASH_LEARNMORE_URI, }, { "l10n-id": "crashed-subframe-submit", popup: null, callback: async () => { if (dumpID) { UnsubmittedCrashHandler.submitReports( [dumpID], lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB ); } closeAllNotifications(); }, }, ]; notification = notificationBox.appendNotification( value, { label: { "l10n-id": "crashed-subframe-message" }, image: TABCRASHED_ICON_URI, priority: notificationBox.PRIORITY_INFO_MEDIUM, eventCallback: eventName => { if (eventName == "disconnected") { let existingItem = this.notificationsMap.get(childID); if (existingItem) { let idx = existingItem.indexOf(notification); if (idx >= 0) { existingItem.splice(idx, 1); } if (!existingItem.length) { this.notificationsMap.delete(childID); } } } else if (eventName == "dismissed") { if (dumpID) { lazy.CrashSubmit.ignore(dumpID); this.childMap.delete(childID); } closeAllNotifications(); } }, }, buttons ); let existingItem = this.notificationsMap.get(childID); if (existingItem) { existingItem.push(notification); } else { this.notificationsMap.set(childID, [notification]); } }, /** * This method is exposed for SessionStore to call if the user selects * a tab which will restore on demand. It's possible that the tab * is in this state because it recently crashed. If that's the case, then * it's also possible that the user has not seen the tab crash page for * that particular crash, in which case, we might show it to them instead * of restoring the tab. * * @param browser () * A browser from a browser tab that the user has just selected * to restore on demand. * @returns (boolean) * True if TabCrashHandler will send the user to the tab crash * page instead. */ willShowCrashedTab(browser) { let childID = this.browserMap.get(browser); // We will only show the tab crash page if: // 1) We are aware that this browser crashed // 2) We know we've never shown the tab crash page for the // crash yet // 3) The user is not configured to automatically submit backlogged // crash reports. If they are, we'll send the crash report // immediately. if (childID && this.unseenCrashedChildIDs.includes(childID)) { if (UnsubmittedCrashHandler.autoSubmit) { let dumpID = this.childMap.get(childID); if (dumpID) { UnsubmittedCrashHandler.submitReports( [dumpID], lazy.CrashSubmit.SUBMITTED_FROM_AUTO ); } } else { this.sendToTabCrashedPage(browser); return true; } } else if (childID === 0) { if (this.restartRequiredBrowsers.has(browser)) { this.sendToRestartRequiredPage(browser); } else { this.sendToTabCrashedPage(browser); } return true; } return false; }, sendToRestartRequiredPage(browser) { let uri = browser.currentURI; let gBrowser = browser.getTabBrowser(); let tab = gBrowser.getTabForBrowser(browser); // The restart required page is non-remote by default. gBrowser.updateBrowserRemoteness(browser, { remoteType: lazy.E10SUtils.NOT_REMOTE, }); browser.docShell.displayLoadError(Cr.NS_ERROR_BUILDID_MISMATCH, uri, null); tab.setAttribute("crashed", true); gBrowser.tabContainer.updateTabIndicatorAttr(tab); // Make sure to only count once even if there are multiple windows // that will all show about:restartrequired. if (this._crashedTabCount == 1) { Services.telemetry.scalarAdd("dom.contentprocess.buildID_mismatch", 1); } }, /** * We show a special page to users when a normal browser tab has crashed. * This method should be called to send a browser to that page once the * process has completely closed. * * @param browser () * The browser that has recently crashed. */ sendToTabCrashedPage(browser) { let title = browser.contentTitle; let uri = browser.currentURI; let gBrowser = browser.getTabBrowser(); let tab = gBrowser.getTabForBrowser(browser); // The tab crashed page is non-remote by default. gBrowser.updateBrowserRemoteness(browser, { remoteType: lazy.E10SUtils.NOT_REMOTE, }); browser.setAttribute("crashedPageTitle", title); browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null); browser.removeAttribute("crashedPageTitle"); tab.setAttribute("crashed", true); gBrowser.tabContainer.updateTabIndicatorAttr(tab); }, /** * Submits a crash report from about:tabcrashed, if the crash * reporter is enabled and a crash report can be found. * * @param browser * The that the report was sent from. * @param message * Message data with the following properties: * * includeURL (bool): * Whether to include the URL that the user was on * in the crashed tab before the crash occurred. * URL (String) * The URL that the user was on in the crashed tab * before the crash occurred. * comments (String): * Any additional comments from the user. * * Note that it is expected that all properties are set, * even if they are empty. */ maybeSendCrashReport(browser, message) { if (!AppConstants.MOZ_CRASHREPORTER) { return; } if (!message.data.hasReport) { // There was no report, so nothing to do. return; } if (message.data.autoSubmit) { // The user has opted in to autosubmitted backlogged // crash reports in the future. UnsubmittedCrashHandler.autoSubmit = true; } let childID = this.browserMap.get(browser); let dumpID = this.childMap.get(childID); if (!dumpID) { return; } if (!message.data.sendReport) { Services.telemetry .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED") .add(1); this.prefs.setBoolPref("sendReport", false); return; } let { includeURL, comments, URL } = message.data; let extraExtraKeyVals = { Comments: comments, URL, }; // For the entries in extraExtraKeyVals, we only want to submit the // extra data values where they are not the empty string. for (let key in extraExtraKeyVals) { let val = extraExtraKeyVals[key].trim(); if (!val) { delete extraExtraKeyVals[key]; } } // URL is special, since it's already been written to extra data by // default. In order to make sure we don't send it, we overwrite it // with the empty string. if (!includeURL) { extraExtraKeyVals.URL = ""; } lazy.CrashSubmit.submit(dumpID, lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB, { recordSubmission: true, extraExtraKeyVals, }).catch(console.error); this.prefs.setBoolPref("sendReport", true); this.prefs.setBoolPref("includeURL", includeURL); this.childMap.set(childID, null); // Avoid resubmission. this.removeSubmitCheckboxesForSameCrash(childID); }, removeSubmitCheckboxesForSameCrash(childID) { for (let window of Services.wm.getEnumerator("navigator:browser")) { if (!window.gMultiProcessBrowser) { continue; } for (let browser of window.gBrowser.browsers) { if (browser.isRemoteBrowser) { continue; } let doc = browser.contentDocument; if (!doc.documentURI.startsWith("about:tabcrashed")) { continue; } if (this.browserMap.get(browser) == childID) { this.browserMap.delete(browser); browser.sendMessageToActor("CrashReportSent", {}, "AboutTabCrashed"); } } } }, /** * Process a crashed tab loaded into a browser. * * @param browser * The containing the page that crashed. * @returns crash data * Message data containing information about the crash. */ onAboutTabCrashedLoad(browser) { this._crashedTabCount++; let window = browser.ownerGlobal; // Reset the zoom for the tabcrashed page. window.ZoomManager.setZoomForBrowser(browser, 1); let childID = this.browserMap.get(browser); let index = this.unseenCrashedChildIDs.indexOf(childID); if (index != -1) { this.unseenCrashedChildIDs.splice(index, 1); } let dumpID = this.getDumpID(browser); if (!dumpID) { return { hasReport: false, }; } let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit; let sendReport = this.prefs.getBoolPref("sendReport"); let includeURL = this.prefs.getBoolPref("includeURL"); let data = { hasReport: true, sendReport, includeURL, requestAutoSubmit, }; return data; }, onAboutTabCrashedUnload(browser) { if (!this._crashedTabCount) { console.error("Can not decrement crashed tab count to below 0"); return; } this._crashedTabCount--; let childID = this.browserMap.get(browser); // Make sure to only count once even if there are multiple windows // that will all show about:tabcrashed. if (this._crashedTabCount == 0 && childID) { Services.telemetry .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED") .add(1); } }, /** * For some , return a crash report dump ID for that browser * if we have been informed of one. Otherwise, return null. * * @param browser ( this.dateString()) { // We'll be suppressing any notifications until after suppressedDate, // so there's no need to do anything more. this.suppressed = true; return; } // We're done suppressing, so we don't need this pref anymore. this.prefs.clearUserPref("suppressUntilDate"); } Services.obs.addObserver(this, "profile-before-change"); } }, uninit() { if (!this.initialized) { return; } this.initialized = false; if (this._checkTimeout) { lazy.clearTimeout(this._checkTimeout); this._checkTimeout = null; } if (!this.enabled) { return; } if (this.suppressed) { this.suppressed = false; // No need to do any more clean-up, since we were suppressed. return; } if (this.showingNotification) { this.prefs.setBoolPref("shutdownWhileShowing", true); this.showingNotification = false; } Services.obs.removeObserver(this, "profile-before-change"); }, observe(subject, topic, data) { switch (topic) { case "profile-before-change": { this.uninit(); break; } } }, scheduleCheckForUnsubmittedCrashReports() { this._checkTimeout = lazy.setTimeout(() => { Services.tm.idleDispatchToMainThread(() => { this.checkForUnsubmittedCrashReports(); }); }, CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS); }, /** * Scans the profile directory for unsubmitted crash reports * within the past PENDING_CRASH_REPORT_DAYS days. If it * finds any, it will, if necessary, attempt to open a notification * bar to prompt the user to submit them. * * @returns Promise * Resolves with the after it tries to * show a notification on the most recent browser window. * If a notification cannot be shown, will resolve with null. */ async checkForUnsubmittedCrashReports() { if (!this.enabled || this.suppressed) { return null; } let dateLimit = new Date(); dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS); let reportIDs = []; try { reportIDs = await lazy.CrashSubmit.pendingIDs(dateLimit); } catch (e) { console.error(e); return null; } if (reportIDs.length) { if (this.autoSubmit) { this.submitReports(reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_AUTO); } else if (this.shouldShowPendingSubmissionsNotification()) { return this.showPendingSubmissionsNotification(reportIDs); } } return null; }, /** * Returns true if the notification should be shown. * shouldShowPendingSubmissionsNotification makes this decision * by looking at whether or not the user has seen the notification * over several days without ever interacting with it. If this occurs * too many times, we suppress the notification for DAYS_TO_SUPPRESS * days. * * @returns bool */ shouldShowPendingSubmissionsNotification() { if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) { return true; } let shutdownWhileShowing = this.prefs.getBoolPref("shutdownWhileShowing"); this.prefs.clearUserPref("shutdownWhileShowing"); if (!this.prefs.prefHasUserValue("lastShownDate")) { // This isn't expected, but we're being defensive here. We'll // opt for showing the notification in this case. return true; } let lastShownDate = this.prefs.getCharPref("lastShownDate"); if (this.dateString() > lastShownDate && shutdownWhileShowing) { // We're on a newer day then when we last showed the // notification without closing it. We don't want to do // this too many times, so we'll decrement a counter for // this situation. Too many of these, and we'll assume the // user doesn't know or care about unsubmitted notifications, // and we'll suppress the notification for a while. let chances = this.prefs.getIntPref("chancesUntilSuppress"); if (--chances < 0) { // We're out of chances! this.prefs.clearUserPref("chancesUntilSuppress"); // We'll suppress for DAYS_TO_SUPPRESS days. let suppressUntil = this.dateString( new Date(Date.now() + DAY * DAYS_TO_SUPPRESS) ); this.prefs.setCharPref("suppressUntilDate", suppressUntil); return false; } this.prefs.setIntPref("chancesUntilSuppress", chances); } return true; }, /** * Given an array of unsubmitted crash report IDs, try to open * up a notification asking the user to submit them. * * @param reportIDs (Array) * The Array of report IDs to offer the user to send. * @returns The if one is shown. null otherwise. */ showPendingSubmissionsNotification(reportIDs) { if (!reportIDs.length) { return null; } let notification = this.show({ notificationID: "pending-crash-reports", reportIDs, onAction: () => { this.showingNotification = false; }, }); if (notification) { this.showingNotification = true; this.prefs.setCharPref("lastShownDate", this.dateString()); } return notification; }, /** * Returns a string representation of a Date in the format * YYYYMMDD. * * @param someDate (Date, optional) * The Date to convert to the string. If not provided, * defaults to today's date. * @returns String */ dateString(someDate = new Date()) { let year = String(someDate.getFullYear()).padStart(4, "0"); let month = String(someDate.getMonth() + 1).padStart(2, "0"); let day = String(someDate.getDate()).padStart(2, "0"); return year + month + day; }, /** * Attempts to show a notification bar to the user in the most * recent browser window asking them to submit some crash report * IDs. If a notification cannot be shown (for example, there * is no browser window), this method exits silently. * * The notification will allow the user to submit their crash * reports. If the user dismissed the notification, the crash * reports will be marked to be ignored (though they can * still be manually submitted via about:crashes). * * @param JS Object * An Object with the following properties: * * notificationID (string) * The ID for the notification to be opened. * * reportIDs (Array) * The array of report IDs to offer to the user. * * onAction (function, optional) * A callback to fire once the user performs an * action on the notification bar (this includes * dismissing the notification). * * @returns The if one is shown. null otherwise. */ show({ notificationID, reportIDs, onAction }) { let chromeWin = lazy.BrowserWindowTracker.getTopWindow(); if (!chromeWin) { // Can't show a notification in this case. We'll hopefully // get another opportunity to have the user submit their // crash reports later. return null; } let notification = chromeWin.gNotificationBox.getNotificationWithValue( notificationID ); if (notification) { return null; } chromeWin.MozXULElement.insertFTLIfNeeded("browser/contentCrash.ftl"); let buttons = [ { "l10n-id": "pending-crash-reports-send", callback: () => { this.submitReports( reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR ); if (onAction) { onAction(); } }, }, { "l10n-id": "pending-crash-reports-always-send", callback: () => { this.autoSubmit = true; this.submitReports( reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR ); if (onAction) { onAction(); } }, }, { "l10n-id": "pending-crash-reports-view-all", callback() { chromeWin.openTrustedLinkIn("about:crashes", "tab"); return true; }, }, ]; let eventCallback = eventType => { if (eventType == "dismissed") { // The user intentionally dismissed the notification, // which we interpret as meaning that they don't care // to submit the reports. We'll ignore these particular // reports going forward. reportIDs.forEach(function(reportID) { lazy.CrashSubmit.ignore(reportID); }); if (onAction) { onAction(); } } }; return chromeWin.gNotificationBox.appendNotification( notificationID, { label: { "l10n-id": "pending-crash-reports-message", "l10n-args": { reportCount: reportIDs.length }, }, image: TABCRASHED_ICON_URI, priority: chromeWin.gNotificationBox.PRIORITY_INFO_HIGH, eventCallback, }, buttons ); }, get autoSubmit() { return Services.prefs.getBoolPref( "browser.crashReports.unsubmittedCheck.autoSubmit2" ); }, set autoSubmit(val) { Services.prefs.setBoolPref( "browser.crashReports.unsubmittedCheck.autoSubmit2", val ); }, /** * Attempt to submit reports to the crash report server. * * @param reportIDs (Array) * The array of reportIDs to submit. * @param submittedFrom (string) * One of the CrashSubmit.SUBMITTED_FROM_* constants representing * how this crash was submitted. */ submitReports(reportIDs, submittedFrom) { for (let reportID of reportIDs) { lazy.CrashSubmit.submit(reportID, submittedFrom).catch(console.error); } }, };