Source code

Revision control

Copy as Markdown

Other Tools

/* 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();
dispatchCustomEvent("CrashSubmitSucceeded");
},
() => {
button.classList.remove("submitting");
button.classList.add("failed-to-submit");
document.l10n.setAttributes(
buttonText,
"submit-crash-button-failure-label"
);
dispatchCustomEvent("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 dispatchCustomEvent(name) {
document.dispatchEvent(
new CustomEvent(name, { bubbles: true, cancelable: false })
);
}
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;
}