summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/content
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/crashreporter/content/crashes.css70
-rw-r--r--toolkit/crashreporter/content/crashes.html99
-rw-r--r--toolkit/crashreporter/content/crashes.js316
3 files changed, 485 insertions, 0 deletions
diff --git a/toolkit/crashreporter/content/crashes.css b/toolkit/crashreporter/content/crashes.css
new file mode 100644
index 0000000000..3f08455005
--- /dev/null
+++ b/toolkit/crashreporter/content/crashes.css
@@ -0,0 +1,70 @@
+/* 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/. */
+
+:root {
+ font-family: sans-serif;
+ margin: 40px auto;
+ min-width: 30em;
+ max-width: 60em;
+}
+
+.hidden {
+ display: none;
+}
+
+/* Table layout */
+
+table {
+ width: 100%;
+ padding-bottom: 2em;
+ border-spacing: 0;
+}
+
+th {
+ text-align: start;
+}
+
+th, td {
+ border-bottom: 1px solid var(--in-content-border-color);
+}
+
+th,
+#submitted td {
+ /* Unsubmitted table already gets spacing from button */
+ padding-block: 10px;
+}
+
+.submit-button,
+.crash-link {
+ float: inline-end;
+}
+
+/* Other elements */
+
+.table-title-container {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+}
+
+.submitting {
+ background-image: url(chrome://global/skin/icons/loading.png);
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 16px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ .submitting {
+ background-image: url(chrome://global/skin/icons/loading@2x.png);
+ }
+}
+
+.submitting .submit-crash-button-label {
+ display: none;
+}
+
+.failed-to-submit {
+ color: #ca8695;
+}
diff --git a/toolkit/crashreporter/content/crashes.html b/toolkit/crashreporter/content/crashes.html
new file mode 100644
index 0000000000..088e01762f
--- /dev/null
+++ b/toolkit/crashreporter/content/crashes.html
@@ -0,0 +1,99 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:; object-src 'none'"
+ />
+ <meta charset="utf-8" />
+ <meta nem="color-scheme" content="light dark" />
+ <link rel="localization" href="crashreporter/aboutcrashes.ftl" />
+ <link rel="stylesheet" href="chrome://global/content/crashes.css" />
+ <link
+ rel="stylesheet"
+ media="screen, projection"
+ href="chrome://global/skin/in-content/common.css"
+ />
+ <script src="chrome://global/content/crashes.js"></script>
+ <title data-l10n-id="crash-reports-title"></title>
+ </head>
+
+ <body>
+ <p id="noConfig" class="hidden" data-l10n-id="no-config-label"></p>
+ <p
+ id="noSubmittedReports"
+ class="hidden"
+ data-l10n-id="no-reports-label"
+ ></p>
+
+ <div id="reportListUnsubmitted" class="hidden">
+ <div class="table-title-container">
+ <h2 data-l10n-id="crashes-unsubmitted-label"></h2>
+ <button
+ id="submitAllUnsubmittedReports"
+ class="submit-button"
+ data-l10n-id="submit-all-button-label"
+ ></button>
+ <button
+ id="clearUnsubmittedReports"
+ data-l10n-id="delete-button-label"
+ ></button>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th data-l10n-id="id-heading"></th>
+ <th data-l10n-id="date-crashed-heading"></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody id="unsubmitted"></tbody>
+ </table>
+ </div>
+
+ <div id="reportListSubmitted" class="hidden">
+ <div class="table-title-container">
+ <h2 data-l10n-id="crashes-submitted-label"></h2>
+ <button
+ id="clearSubmittedReports"
+ data-l10n-id="delete-button-label"
+ ></button>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th data-l10n-id="id-heading"></th>
+ <th data-l10n-id="date-submitted-heading"></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody id="submitted"></tbody>
+ </table>
+ </div>
+ </body>
+
+ <template id="crashReportRow">
+ <tr>
+ <td class="crash-id"></td>
+ <td></td>
+ <td></td>
+ </tr>
+ </template>
+
+ <template id="crashSubmitButton">
+ <button class="submit-button">
+ <span
+ class="submit-crash-button-label"
+ data-l10n-id="submit-crash-button-label"
+ ></span>
+ </button>
+ </template>
+
+ <template id="viewCrashLink">
+ <a class="crash-link" data-l10n-id="view-crash-button-label"></a>
+ </template>
+</html>
diff --git a/toolkit/crashreporter/content/crashes.js b/toolkit/crashreporter/content/crashes.js
new file mode 100644
index 0000000000..fe3b2e7225
--- /dev/null
+++ b/toolkit/crashreporter/content/crashes.js
@@ -0,0 +1,316 @@
+/* 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/. */
+
+let reportURL;
+
+const { CrashReports } = ChromeUtils.importESModule(
+ "resource://gre/modules/CrashReports.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ CrashSubmit: "resource://gre/modules/CrashSubmit.sys.mjs",
+});
+
+document.addEventListener("DOMContentLoaded", () => {
+ populateReportLists();
+ document
+ .getElementById("clearUnsubmittedReports")
+ .addEventListener("click", () => {
+ clearUnsubmittedReports().catch(console.error);
+ });
+ document
+ .getElementById("submitAllUnsubmittedReports")
+ .addEventListener("click", () => {
+ submitAllUnsubmittedReports().catch(console.error);
+ });
+ document
+ .getElementById("clearSubmittedReports")
+ .addEventListener("click", () => {
+ clearSubmittedReports().catch(console.error);
+ });
+});
+
+const buildID = Services.appinfo.appBuildID;
+
+/**
+ * Adds the crash reports with submission buttons and links
+ * to the unsubmitted and submitted crash report lists.
+ * If breakpad.reportURL is not set, displays a misconfiguration message
+ * instead.
+ */
+function populateReportLists() {
+ try {
+ reportURL = Services.prefs.getCharPref("breakpad.reportURL");
+ // Ignore any non http/https urls
+ if (!/^https?:/i.test(reportURL)) {
+ reportURL = null;
+ }
+ } catch (e) {
+ reportURL = null;
+ }
+ if (!reportURL) {
+ document.getElementById("noConfig").classList.remove("hidden");
+ return;
+ }
+
+ const reports = CrashReports.getReports();
+ const dateFormatter = new Services.intl.DateTimeFormat(undefined, {
+ timeStyle: "short",
+ dateStyle: "short",
+ });
+ reports.forEach(report =>
+ addReportRow(report.pending, report.id, report.date, dateFormatter)
+ );
+ showAppropriateSections();
+}
+
+/**
+ * Adds a crash report with the appropriate submission button
+ * or viewing link to the unsubmitted or submitted report list
+ * based on isPending.
+ *
+ * @param {Boolean} isPending whether the crash is up for submission
+ * @param {String} id the unique id of the crash report
+ * @param {Date} date either the date of crash or date of submission
+ * @param {Object} dateFormatter formatter for presenting dates to users
+ */
+function addReportRow(isPending, id, date, dateFormatter) {
+ const rowTemplate = document.getElementById("crashReportRow");
+ const row = document
+ .importNode(rowTemplate.content, true)
+ .querySelector("tr");
+ row.id = id;
+
+ const cells = row.querySelectorAll("td");
+ cells[0].appendChild(document.createTextNode(id));
+ cells[1].appendChild(document.createTextNode(dateFormatter.format(date)));
+
+ if (isPending) {
+ const buttonTemplate = document.getElementById("crashSubmitButton");
+ const button = document
+ .importNode(buttonTemplate.content, true)
+ .querySelector("button");
+ const buttonText = button.querySelector("span");
+ button.addEventListener("click", () =>
+ submitPendingReport(id, row, button, buttonText, dateFormatter)
+ );
+ cells[2].appendChild(button);
+ document.getElementById("unsubmitted").appendChild(row);
+ } else {
+ const linkTemplate = document.getElementById("viewCrashLink");
+ const link = document
+ .importNode(linkTemplate.content, true)
+ .querySelector("a");
+ link.href = `${reportURL}${id}`;
+ cells[2].appendChild(link);
+ document.getElementById("submitted").appendChild(row);
+ }
+}
+
+/**
+ * Shows or hides each of the unsubmitted and submitted report list
+ * based on whether they contain at least one crash report.
+ * If hidden, the submitted report list is replaced by a message
+ * indicating that no crash reports have been submitted.
+ */
+function showAppropriateSections() {
+ let hasUnsubmitted =
+ document.getElementById("unsubmitted").childElementCount > 0;
+ document
+ .getElementById("reportListUnsubmitted")
+ .classList.toggle("hidden", !hasUnsubmitted);
+
+ let hasSubmitted = document.getElementById("submitted").childElementCount > 0;
+ document
+ .getElementById("reportListSubmitted")
+ .classList.toggle("hidden", !hasSubmitted);
+ document
+ .getElementById("noSubmittedReports")
+ .classList.toggle("hidden", hasSubmitted);
+}
+
+/**
+ * Changes the provided button to display a spinner. Then, tries to submit the
+ * crash report for the provided id. On success, removes the crash report from
+ * the list of unsubmitted crash reports and adds a new crash report to the list
+ * of submitted crash reports. On failure, changes the provided button to display
+ * a red error message.
+ *
+ * @param {String} reportId the unique id of the crash report
+ * @param {HTMLTableRowElement} row the table row of the crash report
+ * @param {HTMLButtonElement} button the button pressed to start the submission
+ * @param {HTMLSpanElement} buttonText the text inside the pressed button
+ * @param {Object} dateFormatter formatter for presenting dates to users
+ */
+function submitPendingReport(reportId, row, button, buttonText, dateFormatter) {
+ button.classList.add("submitting");
+ document.getElementById("submitAllUnsubmittedReports").disabled = true;
+ CrashSubmit.submit(reportId, CrashSubmit.SUBMITTED_FROM_ABOUT_CRASHES, {
+ noThrottle: true,
+ })
+ .then(
+ remoteCrashID => {
+ document.getElementById("unsubmitted").removeChild(row);
+ const report = CrashReports.getReports().filter(
+ report => report.id === remoteCrashID
+ );
+ addReportRow(false, remoteCrashID, report.date, dateFormatter);
+ showAppropriateSections();
+ dispatchEvent("CrashSubmitSucceeded");
+ },
+ () => {
+ button.classList.remove("submitting");
+ button.classList.add("failed-to-submit");
+ document.l10n.setAttributes(
+ buttonText,
+ "submit-crash-button-failure-label"
+ );
+ dispatchEvent("CrashSubmitFailed");
+ }
+ )
+ .finally(() => {
+ document.getElementById("submitAllUnsubmittedReports").disabled = false;
+ });
+}
+
+/**
+ * Deletes unsubmitted and old crash reports from the user's device.
+ * Then, hides the list of unsubmitted crash reports.
+ */
+async function clearUnsubmittedReports() {
+ const [title, description] = await document.l10n.formatValues([
+ { id: "delete-confirm-title" },
+ { id: "delete-unsubmitted-description" },
+ ]);
+ if (!Services.prompt.confirm(window, title, description)) {
+ return;
+ }
+
+ await enqueueCleanup(() => cleanupFolder(CrashReports.pendingDir.path));
+ await enqueueCleanup(clearOldReports);
+ document.getElementById("reportListUnsubmitted").classList.add("hidden");
+}
+
+/**
+ * Submits all the pending crash reports and removes all pending reports from pending reports list
+ * and add them to submitted crash reports.
+ */
+async function submitAllUnsubmittedReports() {
+ for (
+ var i = 0;
+ i < document.getElementById("unsubmitted").childNodes.length;
+ i++
+ ) {
+ document
+ .getElementById("unsubmitted")
+ .childNodes[i].cells[2].childNodes[0].click();
+ }
+}
+
+/**
+ * Deletes submitted and old crash reports from the user's device.
+ * Then, hides the list of submitted crash reports.
+ */
+async function clearSubmittedReports() {
+ const [title, description] = await document.l10n.formatValues([
+ { id: "delete-confirm-title" },
+ { id: "delete-submitted-description" },
+ ]);
+ if (!Services.prompt.confirm(window, title, description)) {
+ return;
+ }
+
+ await enqueueCleanup(async () =>
+ cleanupFolder(
+ CrashReports.submittedDir.path,
+ async entry => entry.name.startsWith("bp-") && entry.name.endsWith(".txt")
+ )
+ );
+ await enqueueCleanup(clearOldReports);
+ document.getElementById("reportListSubmitted").classList.add("hidden");
+ document.getElementById("noSubmittedReports").classList.remove("hidden");
+}
+
+/**
+ * Deletes old crash reports from the user's device.
+ */
+async function clearOldReports() {
+ const oneYearAgo = Date.now() - 31586000000;
+ await cleanupFolder(CrashReports.reportsDir.path, async entry => {
+ if (
+ !entry.name.startsWith("InstallTime") ||
+ entry.name == "InstallTime" + buildID
+ ) {
+ return false;
+ }
+
+ const stat = await IOUtils.stat(entry.path);
+ return stat.lastModified < oneYearAgo;
+ });
+}
+
+/**
+ * Deletes files from the user's device at the specified path
+ * that match the provided filter.
+ *
+ * @param {String} path the directory location to delete form
+ * @param {Function} filter function taking in a file entry and
+ * returning whether to delete the file
+ */
+async function cleanupFolder(path, filter) {
+ function entry(path) {
+ return {
+ path,
+ name: PathUtils.filename(path),
+ };
+ }
+ let children;
+ try {
+ children = await IOUtils.getChildren(path);
+ } catch (e) {
+ if (DOMException.isInstance(e) || e.name !== "NotFoundError") {
+ throw e;
+ }
+ }
+
+ for (const childPath of children) {
+ if (!filter || (await filter(entry(childPath)))) {
+ await IOUtils.remove(childPath);
+ }
+ }
+}
+
+/**
+ * Dispatches an event with the specified name.
+ *
+ * @param {String} name the name of the event
+ */
+function dispatchEvent(name) {
+ const event = document.createEvent("Events");
+ event.initEvent(name, true, false);
+ document.dispatchEvent(event);
+}
+
+let cleanupQueue = Promise.resolve();
+
+/**
+ * Enqueue a cleanup function.
+ *
+ * Instead of directly calling cleanup functions as a result of DOM
+ * interactions, queue them through this function so that we do not have
+ * overlapping executions of cleanup functions.
+ *
+ * Cleanup functions overlapping could cause a race where one function is
+ * attempting to stat a file while another function is attempting to delete it,
+ * causing an exception.
+ *
+ * @param fn The cleanup function to call. It will be called once the last
+ * cleanup function has resolved.
+ *
+ * @returns A promise to await instead of awaiting the cleanup function.
+ */
+function enqueueCleanup(fn) {
+ cleanupQueue = cleanupQueue.then(fn);
+ return cleanupQueue;
+}