diff options
Diffstat (limited to 'toolkit/crashreporter/content')
-rw-r--r-- | toolkit/crashreporter/content/crashes.css | 70 | ||||
-rw-r--r-- | toolkit/crashreporter/content/crashes.html | 99 | ||||
-rw-r--r-- | toolkit/crashreporter/content/crashes.js | 316 |
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; +} |