tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ContentCrashHandlers.sys.mjs (40043B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     11  CrashSubmit: "resource://gre/modules/CrashSubmit.sys.mjs",
     12  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     13  RemoteSettingsCrashPull:
     14    "resource://gre/modules/RemoteSettingsCrashPull.sys.mjs",
     15  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     16  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     17  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     18 });
     19 
     20 // We don't process crash reports older than 28 days, so don't bother
     21 // submitting them
     22 const PENDING_CRASH_REPORT_DAYS = 28;
     23 const DAY = 24 * 60 * 60 * 1000; // milliseconds
     24 const DAYS_TO_SUPPRESS = 30;
     25 const MAX_UNSEEN_CRASHED_CHILD_IDS = 20;
     26 const MAX_UNSEEN_CRASHED_SUBFRAME_IDS = 10;
     27 
     28 // Make sure that we will not prompt the user more than once a week
     29 const SILENCE_FOR_DAYS_IN_S = 7 * 86400;
     30 
     31 // Time after which we will begin scanning for unsubmitted crash reports
     32 const CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS = 60 * 10000; // 10 minutes
     33 
     34 // This is SIGUSR1 and indicates a user-invoked crash
     35 const EXIT_CODE_CONTENT_CRASHED = 245;
     36 
     37 const TABCRASHED_ICON_URI = "chrome://browser/skin/tab-crashed.svg";
     38 
     39 const SUBFRAMECRASH_LEARNMORE_URI =
     40  "https://support.mozilla.org/kb/firefox-crashes-troubleshoot-prevent-and-get-help";
     41 
     42 /**
     43 * BrowserWeakMap is exactly like a WeakMap, but expects <xul:browser>
     44 * objects only.
     45 *
     46 * Under the hood, BrowserWeakMap keys the map off of the <xul:browser>
     47 * permanentKey. If, however, the browser has never gotten a permanentKey,
     48 * it falls back to keying on the <xul:browser> element itself.
     49 */
     50 class BrowserWeakMap extends WeakMap {
     51  get(browser) {
     52    if (browser.permanentKey) {
     53      return super.get(browser.permanentKey);
     54    }
     55    return super.get(browser);
     56  }
     57 
     58  set(browser, value) {
     59    if (browser.permanentKey) {
     60      return super.set(browser.permanentKey, value);
     61    }
     62    return super.set(browser, value);
     63  }
     64 
     65  delete(browser) {
     66    if (browser.permanentKey) {
     67      return super.delete(browser.permanentKey);
     68    }
     69    return super.delete(browser);
     70  }
     71 }
     72 
     73 export var TabCrashHandler = {
     74  _crashedTabCount: 0,
     75  childMap: new Map(),
     76  browserMap: new BrowserWeakMap(),
     77  notificationsMap: new Map(),
     78  unseenCrashedChildIDs: [],
     79  pendingSubFrameCrashes: new Map(),
     80  pendingSubFrameCrashesIDs: [],
     81  crashedBrowserQueues: new Map(),
     82  restartRequiredBrowsers: new WeakSet(),
     83  testBuildIDMismatch: false,
     84 
     85  get prefs() {
     86    delete this.prefs;
     87    return (this.prefs = Services.prefs.getBranch(
     88      "browser.tabs.crashReporting."
     89    ));
     90  },
     91 
     92  init() {
     93    if (this.initialized) {
     94      return;
     95    }
     96    this.initialized = true;
     97 
     98    Services.obs.addObserver(this, "ipc:content-shutdown");
     99    Services.obs.addObserver(this, "oop-frameloader-crashed");
    100  },
    101 
    102  observe(aSubject, aTopic) {
    103    switch (aTopic) {
    104      case "ipc:content-shutdown": {
    105        aSubject.QueryInterface(Ci.nsIPropertyBag2);
    106 
    107        if (!aSubject.get("abnormal")) {
    108          return;
    109        }
    110 
    111        let childID = aSubject.get("childID");
    112        let dumpID = aSubject.get("dumpID");
    113 
    114        // Get and remove the subframe crash info first.
    115        let subframeCrashItem = this.getAndRemoveSubframeCrash(childID);
    116 
    117        if (!dumpID) {
    118          Glean.browserContentCrash.dumpUnavailable.add(1);
    119        } else if (AppConstants.MOZ_CRASHREPORTER) {
    120          this.childMap.set(childID, dumpID);
    121 
    122          // If this is a subframe crash, show the crash notification. Only
    123          // show subframe notifications when there is a minidump available.
    124          if (subframeCrashItem) {
    125            let browsers =
    126              ChromeUtils.nondeterministicGetWeakMapKeys(subframeCrashItem) ||
    127              [];
    128            for (let browserItem of browsers) {
    129              let browser = subframeCrashItem.get(browserItem);
    130              if (browser.isConnected && !browser.ownerGlobal.closed) {
    131                this.showSubFrameNotification(browser, childID, dumpID);
    132              }
    133            }
    134          }
    135        }
    136 
    137        if (!this.flushCrashedBrowserQueue(childID)) {
    138          this.unseenCrashedChildIDs.push(childID);
    139          // The elements in unseenCrashedChildIDs will only be removed if
    140          // the tab crash page is shown. However, ipc:content-shutdown might
    141          // be fired for processes for which we'll never show the tab crash
    142          // page - for example, the thumbnailing process. Another case to
    143          // consider is if the user is configured to submit backlogged crash
    144          // reports automatically, and a background tab crashes. In that case,
    145          // we will never show the tab crash page, and never remove the element
    146          // from the list.
    147          //
    148          // Instead of trying to account for all of those cases, we prevent
    149          // this list from getting too large by putting a reasonable upper
    150          // limit on how many childIDs we track. It's unlikely that this
    151          // array would ever get so large as to be unwieldy (that'd be a lot
    152          // or crashes!), but a leak is a leak.
    153          if (
    154            this.unseenCrashedChildIDs.length > MAX_UNSEEN_CRASHED_CHILD_IDS
    155          ) {
    156            this.unseenCrashedChildIDs.shift();
    157          }
    158        }
    159 
    160        // check for environment affecting crash reporting
    161        let shutdown = Services.env.exists("MOZ_CRASHREPORTER_SHUTDOWN");
    162 
    163        if (shutdown) {
    164          dump(
    165            "A content process crashed and MOZ_CRASHREPORTER_SHUTDOWN is " +
    166              "set, shutting down\n"
    167          );
    168          Services.startup.quit(
    169            Ci.nsIAppStartup.eForceQuit,
    170            EXIT_CODE_CONTENT_CRASHED
    171          );
    172        }
    173 
    174        break;
    175      }
    176      case "oop-frameloader-crashed": {
    177        let browser = aSubject.ownerElement;
    178        if (!browser) {
    179          return;
    180        }
    181 
    182        this.browserMap.set(browser, aSubject.childID);
    183        break;
    184      }
    185    }
    186  },
    187 
    188  /**
    189   * This should be called once a content process has finished
    190   * shutting down abnormally. Any tabbrowser browsers that were
    191   * selected at the time of the crash will then be sent to
    192   * the crashed tab page.
    193   *
    194   * @param childID (int)
    195   *        The childID of the content process that just crashed.
    196   * @returns boolean
    197   *        True if one or more browsers were sent to the tab crashed
    198   *        page.
    199   */
    200  flushCrashedBrowserQueue(childID) {
    201    let browserQueue = this.crashedBrowserQueues.get(childID);
    202    if (!browserQueue) {
    203      return false;
    204    }
    205 
    206    this.crashedBrowserQueues.delete(childID);
    207 
    208    let sentBrowser = false;
    209    for (let weakBrowser of browserQueue) {
    210      let browser = weakBrowser.get();
    211      if (browser) {
    212        if (
    213          this.restartRequiredBrowsers.has(browser) ||
    214          this.testBuildIDMismatch
    215        ) {
    216          this.sendToRestartRequiredPage(browser);
    217        } else {
    218          this.sendToTabCrashedPage(browser);
    219        }
    220        sentBrowser = true;
    221      }
    222    }
    223 
    224    return sentBrowser;
    225  },
    226 
    227  /**
    228   * Called by a tabbrowser when it notices that its selected browser
    229   * has crashed. This will queue the browser to show the tab crash
    230   * page once the content process has finished tearing down.
    231   *
    232   * @param browser (<xul:browser>)
    233   *        The selected browser that just crashed.
    234   * @param restartRequired (bool)
    235   *        Whether or not a browser restart is required to recover.
    236   */
    237  onSelectedBrowserCrash(browser, restartRequired) {
    238    if (!browser.isRemoteBrowser) {
    239      console.error("Selected crashed browser is not remote.");
    240      return;
    241    }
    242    if (!browser.frameLoader) {
    243      console.error("Selected crashed browser has no frameloader.");
    244      return;
    245    }
    246 
    247    let childID = browser.frameLoader.childID;
    248 
    249    let browserQueue = this.crashedBrowserQueues.get(childID);
    250    if (!browserQueue) {
    251      browserQueue = [];
    252      this.crashedBrowserQueues.set(childID, browserQueue);
    253    }
    254    // It's probably unnecessary to store this browser as a
    255    // weak reference, since the content process should complete
    256    // its teardown in the same tick of the event loop, and then
    257    // this queue will be flushed. The weak reference is to avoid
    258    // leaking browsers in case anything goes wrong during this
    259    // teardown process.
    260    browserQueue.push(Cu.getWeakReference(browser));
    261 
    262    if (restartRequired) {
    263      this.restartRequiredBrowsers.add(browser);
    264    }
    265 
    266    // In the event that the content process failed to launch, then
    267    // the childID will be 0. In that case, we will never receive
    268    // a dumpID nor an ipc:content-shutdown observer notification,
    269    // so we should flush the queue for childID 0 immediately.
    270    if (childID == 0) {
    271      this.flushCrashedBrowserQueue(0);
    272    }
    273  },
    274 
    275  /**
    276   * Called by a tabbrowser when it notices that a background browser
    277   * has crashed. This will flip its remoteness to non-remote, and attempt
    278   * to revive the crashed tab so that upon selection the tab either shows
    279   * an error page, or automatically restores.
    280   *
    281   * @param browser (<xul:browser>)
    282   *        The background browser that just crashed.
    283   * @param restartRequired (bool)
    284   *        Whether or not a browser restart is required to recover.
    285   */
    286  onBackgroundBrowserCrash(browser, restartRequired) {
    287    if (restartRequired) {
    288      this.restartRequiredBrowsers.add(browser);
    289    }
    290 
    291    let gBrowser = browser.getTabBrowser();
    292    let tab = gBrowser.getTabForBrowser(browser);
    293 
    294    gBrowser.updateBrowserRemoteness(browser, {
    295      remoteType: lazy.E10SUtils.NOT_REMOTE,
    296    });
    297 
    298    lazy.SessionStore.reviveCrashedTab(tab);
    299  },
    300 
    301  /**
    302   * Called when a subframe crashes. If the dump is available, shows a subframe
    303   * crashed notification, otherwise waits for one to be available.
    304   *
    305   * @param browser (<xul:browser>)
    306   *        The browser containing the frame that just crashed.
    307   * @param childId
    308   *        The id of the process that just crashed.
    309   */
    310  async onSubFrameCrash(browser, childID) {
    311    if (!AppConstants.MOZ_CRASHREPORTER) {
    312      return;
    313    }
    314 
    315    // If a crash dump is available, use it. Otherwise, add the child id to the pending
    316    // subframe crashes list, and wait for the crash "ipc:content-shutdown" notification
    317    // to get the minidump. If it never arrives, don't show the notification.
    318    let dumpID = this.childMap.get(childID);
    319    if (dumpID) {
    320      this.showSubFrameNotification(browser, childID, dumpID);
    321    } else {
    322      let item = this.pendingSubFrameCrashes.get(childID);
    323      if (!item) {
    324        item = new BrowserWeakMap();
    325        this.pendingSubFrameCrashes.set(childID, item);
    326 
    327        // Add the childID to an array that only has room for MAX_UNSEEN_CRASHED_SUBFRAME_IDS
    328        // items. If there is no more room, pop the oldest off and remove it. This technique
    329        // is used instead of a timeout.
    330        if (
    331          this.pendingSubFrameCrashesIDs.length >=
    332          MAX_UNSEEN_CRASHED_SUBFRAME_IDS
    333        ) {
    334          let idToDelete = this.pendingSubFrameCrashesIDs.shift();
    335          this.pendingSubFrameCrashes.delete(idToDelete);
    336        }
    337        this.pendingSubFrameCrashesIDs.push(childID);
    338      }
    339      item.set(browser, browser);
    340    }
    341  },
    342 
    343  /**
    344   * Given a childID, retrieve the subframe crash info for it
    345   * from the pendingSubFrameCrashes map. The data is removed
    346   * from the map and returned.
    347   *
    348   * @param childID number
    349   *        childID of the content that crashed.
    350   * @returns subframe crash info added by previous call to onSubFrameCrash.
    351   */
    352  getAndRemoveSubframeCrash(childID) {
    353    let item = this.pendingSubFrameCrashes.get(childID);
    354    if (item) {
    355      this.pendingSubFrameCrashes.delete(childID);
    356      let idx = this.pendingSubFrameCrashesIDs.indexOf(childID);
    357      if (idx >= 0) {
    358        this.pendingSubFrameCrashesIDs.splice(idx, 1);
    359      }
    360    }
    361 
    362    return item;
    363  },
    364 
    365  /**
    366   * Called to indicate that a subframe within a browser has crashed. A notification
    367   * bar will be shown.
    368   *
    369   * @param browser (<xul:browser>)
    370   *        The browser containing the frame that just crashed.
    371   * @param childId
    372   *        The id of the process that just crashed.
    373   * @param dumpID
    374   *        Minidump id of the crash.
    375   */
    376  async showSubFrameNotification(browser, childID, dumpID) {
    377    let gBrowser = browser.getTabBrowser();
    378    let notificationBox = gBrowser.getNotificationBox(browser);
    379 
    380    const value = "subframe-crashed";
    381    let notification = notificationBox.getNotificationWithValue(value);
    382    if (notification) {
    383      // Don't show multiple notifications for a browser.
    384      return;
    385    }
    386 
    387    let closeAllNotifications = () => {
    388      // Close all other notifications on other tabs that might
    389      // be open for the same crashed process.
    390      let existingItem = this.notificationsMap.get(childID);
    391      if (existingItem) {
    392        for (let notif of existingItem.slice()) {
    393          notif.close();
    394        }
    395      }
    396    };
    397 
    398    gBrowser.ownerGlobal.MozXULElement.insertFTLIfNeeded(
    399      "browser/contentCrash.ftl"
    400    );
    401 
    402    let buttons = [
    403      {
    404        "l10n-id": "crashed-subframe-learnmore-link",
    405        popup: null,
    406        link: SUBFRAMECRASH_LEARNMORE_URI,
    407      },
    408      {
    409        "l10n-id": "crashed-subframe-submit",
    410        popup: null,
    411        callback: async () => {
    412          if (dumpID) {
    413            UnsubmittedCrashHandler.submitReports(
    414              [dumpID],
    415              lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB
    416            );
    417          }
    418          closeAllNotifications();
    419        },
    420      },
    421    ];
    422 
    423    notification = await notificationBox.appendNotification(
    424      value,
    425      {
    426        label: { "l10n-id": "crashed-subframe-message" },
    427        image: TABCRASHED_ICON_URI,
    428        priority: notificationBox.PRIORITY_INFO_MEDIUM,
    429        eventCallback: eventName => {
    430          if (eventName == "disconnected") {
    431            let existingItem = this.notificationsMap.get(childID);
    432            if (existingItem) {
    433              let idx = existingItem.indexOf(notification);
    434              if (idx >= 0) {
    435                existingItem.splice(idx, 1);
    436              }
    437 
    438              if (!existingItem.length) {
    439                this.notificationsMap.delete(childID);
    440              }
    441            }
    442          } else if (eventName == "dismissed") {
    443            if (dumpID) {
    444              lazy.CrashSubmit.ignore(dumpID);
    445              this.childMap.delete(childID);
    446            }
    447 
    448            closeAllNotifications();
    449          }
    450        },
    451      },
    452      buttons
    453    );
    454 
    455    let existingItem = this.notificationsMap.get(childID);
    456    if (existingItem) {
    457      existingItem.push(notification);
    458    } else {
    459      this.notificationsMap.set(childID, [notification]);
    460    }
    461  },
    462 
    463  /**
    464   * This method is exposed for SessionStore to call if the user selects
    465   * a tab which will restore on demand. It's possible that the tab
    466   * is in this state because it recently crashed. If that's the case, then
    467   * it's also possible that the user has not seen the tab crash page for
    468   * that particular crash, in which case, we might show it to them instead
    469   * of restoring the tab.
    470   *
    471   * @param browser (<xul:browser>)
    472   *        A browser from a browser tab that the user has just selected
    473   *        to restore on demand.
    474   * @returns (boolean)
    475   *        True if TabCrashHandler will send the user to the tab crash
    476   *        page instead.
    477   */
    478  willShowCrashedTab(browser) {
    479    let childID = this.browserMap.get(browser);
    480    // We will only show the tab crash page if:
    481    // 1) We are aware that this browser crashed
    482    // 2) We know we've never shown the tab crash page for the
    483    //    crash yet
    484    // 3) The user is not configured to automatically submit backlogged
    485    //    crash reports. If they are, we'll send the crash report
    486    //    immediately.
    487    if (childID && this.unseenCrashedChildIDs.includes(childID)) {
    488      if (UnsubmittedCrashHandler.autoSubmit) {
    489        let dumpID = this.childMap.get(childID);
    490        if (dumpID) {
    491          UnsubmittedCrashHandler.submitReports(
    492            [dumpID],
    493            lazy.CrashSubmit.SUBMITTED_FROM_AUTO
    494          );
    495        }
    496      } else {
    497        this.sendToTabCrashedPage(browser);
    498        return true;
    499      }
    500    } else if (childID === 0) {
    501      if (this.restartRequiredBrowsers.has(browser)) {
    502        this.sendToRestartRequiredPage(browser);
    503      } else {
    504        this.sendToTabCrashedPage(browser);
    505      }
    506      return true;
    507    }
    508 
    509    return false;
    510  },
    511 
    512  sendToRestartRequiredPage(browser) {
    513    let uri = browser.currentURI;
    514    let gBrowser = browser.getTabBrowser();
    515    let tab = gBrowser.getTabForBrowser(browser);
    516    // The restart required page is non-remote by default.
    517    gBrowser.updateBrowserRemoteness(browser, {
    518      remoteType: lazy.E10SUtils.NOT_REMOTE,
    519    });
    520 
    521    browser.docShell.displayLoadError(Cr.NS_ERROR_BUILDID_MISMATCH, uri, null);
    522    tab.setAttribute("crashed", true);
    523  },
    524 
    525  /**
    526   * We show a special page to users when a normal browser tab has crashed.
    527   * This method should be called to send a browser to that page once the
    528   * process has completely closed.
    529   *
    530   * @param browser (<xul:browser>)
    531   *        The browser that has recently crashed.
    532   */
    533  sendToTabCrashedPage(browser) {
    534    let title = browser.contentTitle;
    535    let uri = browser.currentURI;
    536    let gBrowser = browser.getTabBrowser();
    537    let tab = gBrowser.getTabForBrowser(browser);
    538    // The tab crashed page is non-remote by default.
    539    gBrowser.updateBrowserRemoteness(browser, {
    540      remoteType: lazy.E10SUtils.NOT_REMOTE,
    541    });
    542 
    543    browser.setAttribute("crashedPageTitle", title);
    544    browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
    545    browser.removeAttribute("crashedPageTitle");
    546    tab.setAttribute("crashed", true);
    547  },
    548 
    549  /**
    550   * Submits a crash report from about:tabcrashed, if the crash
    551   * reporter is enabled and a crash report can be found.
    552   *
    553   * @param browser
    554   *        The <xul:browser> that the report was sent from.
    555   * @param message
    556   *        Message data with the following properties:
    557   *
    558   *        includeURL (bool):
    559   *          Whether to include the URL that the user was on
    560   *          in the crashed tab before the crash occurred.
    561   *        URL (String)
    562   *          The URL that the user was on in the crashed tab
    563   *          before the crash occurred.
    564   *        comments (String):
    565   *          Any additional comments from the user.
    566   *
    567   *        Note that it is expected that all properties are set,
    568   *        even if they are empty.
    569   */
    570  maybeSendCrashReport(browser, message) {
    571    if (!AppConstants.MOZ_CRASHREPORTER) {
    572      return;
    573    }
    574 
    575    if (!message.data.hasReport) {
    576      // There was no report, so nothing to do.
    577      return;
    578    }
    579 
    580    if (message.data.autoSubmit) {
    581      // The user has opted in to autosubmitted backlogged
    582      // crash reports in the future.
    583      UnsubmittedCrashHandler.autoSubmit = true;
    584    }
    585 
    586    let childID = this.browserMap.get(browser);
    587    let dumpID = this.childMap.get(childID);
    588    if (!dumpID) {
    589      return;
    590    }
    591 
    592    if (!message.data.sendReport) {
    593      Glean.browserContentCrash.notSubmitted.add(1);
    594      this.prefs.setBoolPref("sendReport", false);
    595      return;
    596    }
    597 
    598    // eslint-disable-next-line no-shadow
    599    let { includeURL, comments, URL } = message.data;
    600 
    601    let extraExtraKeyVals = {
    602      Comments: comments,
    603      URL,
    604    };
    605 
    606    // For the entries in extraExtraKeyVals, we only want to submit the
    607    // extra data values where they are not the empty string.
    608    for (let key in extraExtraKeyVals) {
    609      let val = extraExtraKeyVals[key].trim();
    610      if (!val) {
    611        delete extraExtraKeyVals[key];
    612      }
    613    }
    614 
    615    // URL is special, since it's already been written to extra data by
    616    // default. In order to make sure we don't send it, we overwrite it
    617    // with the empty string.
    618    if (!includeURL) {
    619      extraExtraKeyVals.URL = "";
    620    }
    621 
    622    lazy.CrashSubmit.submit(dumpID, lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB, {
    623      recordSubmission: true,
    624      extraExtraKeyVals,
    625    }).catch(console.error);
    626 
    627    this.prefs.setBoolPref("sendReport", true);
    628    this.prefs.setBoolPref("includeURL", includeURL);
    629 
    630    this.childMap.set(childID, null); // Avoid resubmission.
    631    this.removeSubmitCheckboxesForSameCrash(childID);
    632  },
    633 
    634  removeSubmitCheckboxesForSameCrash(childID) {
    635    for (let window of Services.wm.getEnumerator("navigator:browser")) {
    636      if (!window.gMultiProcessBrowser) {
    637        continue;
    638      }
    639 
    640      for (let browser of window.gBrowser.browsers) {
    641        if (browser.isRemoteBrowser) {
    642          continue;
    643        }
    644 
    645        let doc = browser.contentDocument;
    646        if (!doc.documentURI.startsWith("about:tabcrashed")) {
    647          continue;
    648        }
    649 
    650        if (this.browserMap.get(browser) == childID) {
    651          this.browserMap.delete(browser);
    652          browser.sendMessageToActor("CrashReportSent", {}, "AboutTabCrashed");
    653        }
    654      }
    655    }
    656  },
    657 
    658  /**
    659   * Process a crashed tab loaded into a browser.
    660   *
    661   * @param browser
    662   *        The <xul:browser> containing the page that crashed.
    663   * @returns crash data
    664   *        Message data containing information about the crash.
    665   */
    666  onAboutTabCrashedLoad(browser) {
    667    this._crashedTabCount++;
    668 
    669    let window = browser.ownerGlobal;
    670 
    671    // Reset the zoom for the tabcrashed page.
    672    window.ZoomManager.setZoomForBrowser(browser, 1);
    673 
    674    let childID = this.browserMap.get(browser);
    675    let index = this.unseenCrashedChildIDs.indexOf(childID);
    676    if (index != -1) {
    677      this.unseenCrashedChildIDs.splice(index, 1);
    678    }
    679 
    680    let dumpID = this.getDumpID(browser);
    681    if (!dumpID) {
    682      return {
    683        hasReport: false,
    684      };
    685    }
    686 
    687    let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit;
    688    let sendReport = this.prefs.getBoolPref("sendReport");
    689    let includeURL = this.prefs.getBoolPref("includeURL");
    690 
    691    let data = {
    692      hasReport: true,
    693      sendReport,
    694      includeURL,
    695      requestAutoSubmit,
    696    };
    697 
    698    return data;
    699  },
    700 
    701  onAboutTabCrashedUnload(browser) {
    702    if (!this._crashedTabCount) {
    703      console.error("Can not decrement crashed tab count to below 0");
    704      return;
    705    }
    706    this._crashedTabCount--;
    707 
    708    let childID = this.browserMap.get(browser);
    709 
    710    // Make sure to only count once even if there are multiple windows
    711    // that will all show about:tabcrashed.
    712    if (this._crashedTabCount == 0 && childID) {
    713      Glean.browserContentCrash.notSubmitted.add(1);
    714    }
    715  },
    716 
    717  /**
    718   * For some <xul:browser>, return a crash report dump ID for that browser
    719   * if we have been informed of one. Otherwise, return null.
    720   *
    721   * @param browser (<xul:browser)
    722   *        The browser to try to get the dump ID for
    723   * @returns dumpID (String)
    724   */
    725  getDumpID(browser) {
    726    if (!AppConstants.MOZ_CRASHREPORTER) {
    727      return null;
    728    }
    729 
    730    return this.childMap.get(this.browserMap.get(browser));
    731  },
    732 
    733  /**
    734   * This is intended for TESTING ONLY. It returns the amount of
    735   * content processes that have crashed such that we're still waiting
    736   * for dump IDs for their crash reports.
    737   *
    738   * For our automated tests, accessing the crashed content process
    739   * count helps us test the behaviour when content processes crash due
    740   * to launch failure, since in those cases we should not increase the
    741   * crashed browser queue (since we never receive dump IDs for launch
    742   * failures).
    743   */
    744  get queuedCrashedBrowsers() {
    745    return this.crashedBrowserQueues.size;
    746  },
    747 };
    748 
    749 /**
    750 * This component is responsible for scanning the pending
    751 * crash report directory for reports, and (if enabled), to
    752 * prompt the user to submit those reports. It might also
    753 * submit those reports automatically without prompting if
    754 * the user has opted in.
    755 */
    756 export var UnsubmittedCrashHandler = {
    757  get prefs() {
    758    delete this.prefs;
    759    return (this.prefs = Services.prefs.getBranch(
    760      "browser.crashReports.unsubmittedCheck."
    761    ));
    762  },
    763 
    764  get enabled() {
    765    return this.prefs.getBoolPref("enabled");
    766  },
    767 
    768  // showingNotification is set to true once a notification
    769  // is successfully shown, and then set back to false if
    770  // the notification is dismissed by an action by the user.
    771  showingNotification: false,
    772  // suppressed is true if we've determined that we've shown
    773  // the notification too many times across too many days without
    774  // user interaction, so we're suppressing the notification for
    775  // some number of days. See the documentation for
    776  // shouldShowPendingSubmissionsNotification().
    777  suppressed: false,
    778 
    779  _checkTimeout: null,
    780 
    781  log: null,
    782 
    783  init() {
    784    if (this.initialized) {
    785      return;
    786    }
    787 
    788    this.initialized = true;
    789 
    790    this.log = console.createInstance({
    791      prefix: "UnsubmittedCrashHandler",
    792      maxLogLevel: this.prefs.getStringPref("loglevel", "Error"),
    793    });
    794 
    795    // UnsubmittedCrashHandler can be initialized but still be disabled.
    796    // This is intentional, as this makes simulating UnsubmittedCrashHandler's
    797    // reactions to browser startup and shutdown easier in test automation.
    798    //
    799    // UnsubmittedCrashHandler, when initialized but not enabled, is inert.
    800    if (this.enabled) {
    801      lazy.RemoteSettingsCrashPull.start(
    802        this.showRequestedSubmissionsNotification.bind(this)
    803      );
    804      if (this.prefs.prefHasUserValue("suppressUntilDate")) {
    805        if (this.prefs.getCharPref("suppressUntilDate") > this.dateString()) {
    806          // We'll be suppressing any notifications until after suppressedDate,
    807          // so there's no need to do anything more.
    808          this.suppressed = true;
    809          this.log.debug("suppressing crash handler due to suppressUntilDate");
    810          return;
    811        }
    812 
    813        // We're done suppressing, so we don't need this pref anymore.
    814        this.prefs.clearUserPref("suppressUntilDate");
    815      }
    816 
    817      Services.obs.addObserver(this, "profile-before-change");
    818    } else {
    819      this.log.debug("not enabled");
    820    }
    821  },
    822 
    823  uninit() {
    824    if (!this.initialized) {
    825      return;
    826    }
    827 
    828    this.initialized = false;
    829 
    830    this.log = null;
    831 
    832    if (this._checkTimeout) {
    833      lazy.clearTimeout(this._checkTimeout);
    834      this._checkTimeout = null;
    835    }
    836 
    837    if (!this.enabled) {
    838      return;
    839    }
    840 
    841    lazy.RemoteSettingsCrashPull.stop();
    842 
    843    if (this.suppressed) {
    844      this.suppressed = false;
    845      // No need to do any more clean-up, since we were suppressed.
    846      return;
    847    }
    848 
    849    if (this.showingNotification) {
    850      this.prefs.setBoolPref("shutdownWhileShowing", true);
    851      this.showingNotification = false;
    852    }
    853 
    854    Services.obs.removeObserver(this, "profile-before-change");
    855  },
    856 
    857  observe(subject, topic) {
    858    switch (topic) {
    859      case "profile-before-change": {
    860        this.uninit();
    861        break;
    862      }
    863    }
    864  },
    865 
    866  scheduleCheckForUnsubmittedCrashReports() {
    867    this._checkTimeout = lazy.setTimeout(() => {
    868      Services.tm.idleDispatchToMainThread(() => {
    869        this.checkForUnsubmittedCrashReports();
    870      });
    871    }, CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS);
    872  },
    873 
    874  /**
    875   * Scans the profile directory for unsubmitted crash reports
    876   * within the past PENDING_CRASH_REPORT_DAYS days. If it
    877   * finds any, it will, if necessary, attempt to open a notification
    878   * bar to prompt the user to submit them.
    879   *
    880   * @returns Promise
    881   *          Resolves with the <xul:notification> after it tries to
    882   *          show a notification on the most recent browser window.
    883   *          If a notification cannot be shown, will resolve with null.
    884   */
    885  async checkForUnsubmittedCrashReports() {
    886    if (!this.enabled || this.suppressed) {
    887      return null;
    888    }
    889 
    890    this.log.debug("checking for unsubmitted crash reports");
    891 
    892    let dateLimit = new Date();
    893    dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
    894 
    895    let reportIDs = [];
    896    try {
    897      reportIDs = await lazy.CrashSubmit.pendingIDs(dateLimit);
    898    } catch (e) {
    899      this.log.error(e);
    900      return null;
    901    }
    902 
    903    if (reportIDs.length) {
    904      this.log.debug("found ", reportIDs.length, " unsubmitted crash reports");
    905      Glean.crashSubmission.pending.add(reportIDs.length);
    906      if (this.autoSubmit) {
    907        this.log.debug("auto submitted crash reports");
    908        this.submitReports(reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_AUTO);
    909      } else if (this.shouldShowPendingSubmissionsNotification()) {
    910        return this.showPendingSubmissionsNotification(reportIDs);
    911      }
    912    }
    913    return null;
    914  },
    915 
    916  /**
    917   * Returns true if the notification should be shown.
    918   * shouldShowPendingSubmissionsNotification makes this decision
    919   * by looking at whether or not the user has seen the notification
    920   * over several days without ever interacting with it. If this occurs
    921   * too many times, we suppress the notification for DAYS_TO_SUPPRESS
    922   * days.
    923   *
    924   * @returns bool
    925   */
    926  shouldShowPendingSubmissionsNotification() {
    927    // If user is already presented with a remote settings request, do not show
    928    if (this._requestedSubmission.notification) {
    929      return false;
    930    }
    931 
    932    if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) {
    933      return true;
    934    }
    935 
    936    let shutdownWhileShowing = this.prefs.getBoolPref("shutdownWhileShowing");
    937    this.prefs.clearUserPref("shutdownWhileShowing");
    938 
    939    if (!this.prefs.prefHasUserValue("lastShownDate")) {
    940      // This isn't expected, but we're being defensive here. We'll
    941      // opt for showing the notification in this case.
    942      return true;
    943    }
    944 
    945    let lastShownDate = this.prefs.getCharPref("lastShownDate");
    946    if (this.dateString() > lastShownDate && shutdownWhileShowing) {
    947      // We're on a newer day then when we last showed the
    948      // notification without closing it. We don't want to do
    949      // this too many times, so we'll decrement a counter for
    950      // this situation. Too many of these, and we'll assume the
    951      // user doesn't know or care about unsubmitted notifications,
    952      // and we'll suppress the notification for a while.
    953      let chances = this.prefs.getIntPref("chancesUntilSuppress");
    954      if (--chances < 0) {
    955        // We're out of chances!
    956        this.prefs.clearUserPref("chancesUntilSuppress");
    957        // We'll suppress for DAYS_TO_SUPPRESS days.
    958        let suppressUntil = this.dateString(
    959          new Date(Date.now() + DAY * DAYS_TO_SUPPRESS)
    960        );
    961        this.prefs.setCharPref("suppressUntilDate", suppressUntil);
    962        return false;
    963      }
    964      this.prefs.setIntPref("chancesUntilSuppress", chances);
    965    }
    966 
    967    return true;
    968  },
    969 
    970  /**
    971   * Given an array of unsubmitted crash report IDs, try to open
    972   * up a notification asking the user to submit them.
    973   *
    974   * @param reportIDs (Array<string>)
    975   *        The Array of report IDs to offer the user to send.
    976   * @returns The <xul:notification> if one is shown. null otherwise.
    977   */
    978  async showPendingSubmissionsNotification(reportIDs) {
    979    if (!reportIDs.length) {
    980      return null;
    981    }
    982 
    983    this.log.debug("showing pending submissions notification");
    984 
    985    let notification = await this.show({
    986      notificationID: "pending-crash-reports",
    987      reportIDs,
    988      onAction: () => {
    989        this.showingNotification = false;
    990      },
    991    });
    992 
    993    if (notification) {
    994      this.showingNotification = true;
    995      this.prefs.setCharPref("lastShownDate", this.dateString());
    996    }
    997 
    998    return notification;
    999  },
   1000 
   1001  removeExistingNotification(aNotification) {
   1002    if (aNotification) {
   1003      let chromeWin = lazy.BrowserWindowTracker.getTopWindow({
   1004        allowFromInactiveWorkspace: true,
   1005      });
   1006      if (!chromeWin) {
   1007        return false;
   1008      }
   1009 
   1010      chromeWin.gNotificationBox.removeNotification(aNotification, true);
   1011      aNotification = null;
   1012    }
   1013    return true;
   1014  },
   1015 
   1016  _requestedSubmission: {
   1017    notification: null,
   1018    reportIDs: [],
   1019  },
   1020 
   1021  /**
   1022   * Given an array of interesting unsubmitted crash report IDs, try to open
   1023   * up a notification asking the user to submit them.
   1024   *
   1025   * @param newReportIDs (Array<string>)
   1026   *        The Array of report IDs to offer the user to send.
   1027   * @returns The <xul:notification> if one is shown. null otherwise.
   1028   */
   1029  async showRequestedSubmissionsNotification(newReportIDs) {
   1030    if (!newReportIDs.length) {
   1031      return null;
   1032    }
   1033 
   1034    this.log.debug("showing requested pending notification");
   1035 
   1036    if (
   1037      !this.removeExistingNotification(this._requestedSubmission.notification)
   1038    ) {
   1039      return null;
   1040    }
   1041 
   1042    const dontShowBefore = Services.prefs.getIntPref(
   1043      "browser.crashReports.dontShowBefore"
   1044    );
   1045    const now = Math.trunc(Date.now() / 1000);
   1046    if (now < dontShowBefore) {
   1047      return null;
   1048    }
   1049 
   1050    this._requestedSubmission.reportIDs.push(...newReportIDs);
   1051    this._requestedSubmission.notification = await this.show({
   1052      notificationID: "pending-crash-reports-req",
   1053      reportIDs: this._requestedSubmission.reportIDs,
   1054      onAction: () => {
   1055        this._requestedSubmission.notification = null;
   1056        this._requestedSubmission.reportIDs = [];
   1057        Services.prefs.setIntPref(
   1058          "browser.crashReports.dontShowBefore",
   1059          now + SILENCE_FOR_DAYS_IN_S
   1060        );
   1061      },
   1062      requestedByDevs: true,
   1063    });
   1064 
   1065    return this._requestedSubmission.notification;
   1066  },
   1067 
   1068  /**
   1069   * Returns a string representation of a Date in the format
   1070   * YYYYMMDD.
   1071   *
   1072   * @param someDate (Date, optional)
   1073   *        The Date to convert to the string. If not provided,
   1074   *        defaults to today's date.
   1075   * @returns String
   1076   */
   1077  dateString(someDate = new Date()) {
   1078    let year = String(someDate.getFullYear()).padStart(4, "0");
   1079    let month = String(someDate.getMonth() + 1).padStart(2, "0");
   1080    let day = String(someDate.getDate()).padStart(2, "0");
   1081    return year + month + day;
   1082  },
   1083 
   1084  /**
   1085   * Attempts to show a notification bar to the user in the most
   1086   * recent browser window asking them to submit some crash report
   1087   * IDs. If a notification cannot be shown (for example, there
   1088   * is no browser window), this method exits silently.
   1089   *
   1090   * The notification will allow the user to submit their crash
   1091   * reports. If the user dismissed the notification, the crash
   1092   * reports will be marked to be ignored (though they can
   1093   * still be manually submitted via about:crashes).
   1094   *
   1095   * @param JS Object
   1096   *        An Object with the following properties:
   1097   *
   1098   *        notificationID (string)
   1099   *          The ID for the notification to be opened.
   1100   *
   1101   *        reportIDs (Array<string>)
   1102   *          The array of report IDs to offer to the user.
   1103   *
   1104   *        onAction (function, optional)
   1105   *          A callback to fire once the user performs an
   1106   *          action on the notification bar (this includes
   1107   *          dismissing the notification).
   1108   *
   1109   *        requestedByDevs (boolean)
   1110   *          Does it match a Remote Settings request.
   1111   *          If true, a few impactful differences on the notification:
   1112   *           - message will have a different wording
   1113   *           - will not include the "always send" and "view all" buttons.
   1114   *           - when clicking "send", the crash reports will disable throttling
   1115   *
   1116   * @returns The <xul:notification> if one is shown. null otherwise.
   1117   */
   1118  show({ notificationID, reportIDs, onAction, requestedByDevs }) {
   1119    let chromeWin = lazy.BrowserWindowTracker.getTopWindow({
   1120      allowFromInactiveWorkspace: true,
   1121    });
   1122    if (!chromeWin) {
   1123      // Can't show a notification in this case. We'll hopefully
   1124      // get another opportunity to have the user submit their
   1125      // crash reports later.
   1126      return null;
   1127    }
   1128 
   1129    let notification =
   1130      chromeWin.gNotificationBox.getNotificationWithValue(notificationID);
   1131    if (notification) {
   1132      return null;
   1133    }
   1134 
   1135    chromeWin.MozXULElement.insertFTLIfNeeded("browser/contentCrash.ftl");
   1136 
   1137    const pendingCrashReportsSend = {
   1138      "l10n-id": "pending-crash-reports-send",
   1139      callback: () => {
   1140        this.submitReports(
   1141          reportIDs,
   1142          lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR,
   1143          requestedByDevs ? { noThrottle: true } : {}
   1144        );
   1145        if (onAction) {
   1146          onAction();
   1147        }
   1148      },
   1149    };
   1150    const pendingCrashReportsAlwaysSend = {
   1151      "l10n-id": "pending-crash-reports-always-send",
   1152      callback: () => {
   1153        this.autoSubmit = true;
   1154        this.submitReports(reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR);
   1155        if (onAction) {
   1156          onAction();
   1157        }
   1158      },
   1159    };
   1160    const pendingCrashReportsViewAll = {
   1161      "l10n-id": "pending-crash-reports-view-all",
   1162      callback() {
   1163        chromeWin.openTrustedLinkIn("about:crashes", "tab");
   1164        return true;
   1165      },
   1166    };
   1167 
   1168    const requestedCrashSupport = {
   1169      supportPage: "unsent-crash-reports-in-firefox",
   1170    };
   1171    const requestedCrashReportsDontShowAgain = {
   1172      "l10n-id": "requested-crash-reports-dont-show-again",
   1173      callback: () => {
   1174        this.requestedNeverShowAgain = true;
   1175        if (onAction) {
   1176          onAction();
   1177        }
   1178      },
   1179    };
   1180 
   1181    let buttons = [];
   1182    if (requestedByDevs) {
   1183      // Somehow needs to be the first one
   1184      buttons.push(requestedCrashSupport);
   1185    }
   1186 
   1187    buttons.push(pendingCrashReportsSend);
   1188 
   1189    if (!requestedByDevs) {
   1190      buttons.push(pendingCrashReportsAlwaysSend, pendingCrashReportsViewAll);
   1191    } else {
   1192      buttons.push(requestedCrashReportsDontShowAgain);
   1193    }
   1194 
   1195    let eventCallback = eventType => {
   1196      if (eventType == "dismissed") {
   1197        // The user intentionally dismissed the notification,
   1198        // which we interpret as meaning that they don't care
   1199        // to submit the reports. We'll ignore these particular
   1200        // reports going forward.
   1201        reportIDs.forEach(function (reportID) {
   1202          lazy.CrashSubmit.ignore(reportID);
   1203        });
   1204        if (onAction) {
   1205          onAction();
   1206        }
   1207      }
   1208    };
   1209 
   1210    return chromeWin.gNotificationBox.appendNotification(
   1211      notificationID,
   1212      {
   1213        label: {
   1214          "l10n-id": requestedByDevs
   1215            ? "requested-crash-reports-message-new"
   1216            : "pending-crash-reports-message",
   1217          "l10n-args": { reportCount: reportIDs.length },
   1218        },
   1219        image: TABCRASHED_ICON_URI,
   1220        priority: chromeWin.gNotificationBox.PRIORITY_INFO_HIGH,
   1221        eventCallback,
   1222      },
   1223      buttons
   1224    );
   1225  },
   1226 
   1227  get autoSubmit() {
   1228    return Services.prefs.getBoolPref(
   1229      "browser.crashReports.unsubmittedCheck.autoSubmit2"
   1230    );
   1231  },
   1232 
   1233  set autoSubmit(val) {
   1234    Services.prefs.setBoolPref(
   1235      "browser.crashReports.unsubmittedCheck.autoSubmit2",
   1236      val
   1237    );
   1238  },
   1239 
   1240  set requestedNeverShowAgain(val) {
   1241    Services.prefs.setBoolPref(
   1242      "browser.crashReports.requestedNeverShowAgain",
   1243      val
   1244    );
   1245  },
   1246 
   1247  /**
   1248   * Attempt to submit reports to the crash report server.
   1249   *
   1250   * @param reportIDs (Array<string>)
   1251   *        The array of reportIDs to submit.
   1252   * @param submittedFrom (string)
   1253   *        One of the CrashSubmit.SUBMITTED_FROM_* constants representing
   1254   *        how this crash was submitted.
   1255   * @param params (object)
   1256   *        Parameters to be passed to CrashSubmit.submit(), e.g. 'noThrottle'.
   1257   */
   1258  submitReports(reportIDs, submittedFrom, params) {
   1259    this.log.debug(
   1260      "submitting ",
   1261      reportIDs.length,
   1262      " reports from ",
   1263      submittedFrom
   1264    );
   1265    for (let reportID of reportIDs) {
   1266      lazy.CrashSubmit.submit(reportID, submittedFrom, params).catch(
   1267        this.log.error.bind(this.log)
   1268      );
   1269    }
   1270  },
   1271 };