summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutthirdparty/content/aboutThirdParty.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutthirdparty/content/aboutThirdParty.js')
-rw-r--r--toolkit/components/aboutthirdparty/content/aboutThirdParty.js705
1 files changed, 705 insertions, 0 deletions
diff --git a/toolkit/components/aboutthirdparty/content/aboutThirdParty.js b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js
new file mode 100644
index 0000000000..eb1439eda8
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/content/aboutThirdParty.js
@@ -0,0 +1,705 @@
+/* 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";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { ProcessType } = ChromeUtils.importESModule(
+ "resource://gre/modules/ProcessType.sys.mjs"
+);
+
+let AboutThirdParty = null;
+let CrashModuleSet = null;
+let gBackgroundTasksDone = false;
+
+function moduleCompareForDisplay(a, b) {
+ // First, show blocked modules that were blocked at launch - this will keep the ordering
+ // consistent when the user blocks/unblocks things.
+ const bBlocked =
+ b.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
+ ? 1
+ : 0;
+ const aBlocked =
+ a.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
+ ? 1
+ : 0;
+
+ let diff = bBlocked - aBlocked;
+ if (diff) {
+ return diff;
+ }
+
+ // Next, show crasher modules
+ diff = b.isCrasher - a.isCrasher;
+ if (diff) {
+ return diff;
+ }
+
+ // Then unknown-type modules
+ diff = a.typeFlags - b.typeFlags;
+ if (diff) {
+ return diff;
+ }
+
+ // Lastly sort the remaining modules in descending order
+ // of duration to move up slower modules.
+ return b.loadingOnMain - a.loadingOnMain;
+}
+
+async function fetchData() {
+ let data = null;
+ try {
+ // Wait until the module load events are ready (bug 1833152)
+ const sleep = delayInMs =>
+ new Promise(resolve => setTimeout(resolve, delayInMs));
+ let loadEventsReady = Services.telemetry.areUntrustedModuleLoadEventsReady;
+ let numberOfAttempts = 0;
+ // Just to make sure we don't infinite loop here. (this is normally quite
+ // quick) If we do hit this limit, the page will return an empty list of
+ // modules.
+ const MAX_ATTEMPTS = 30;
+ while (!loadEventsReady && numberOfAttempts < MAX_ATTEMPTS) {
+ await sleep(1000);
+ numberOfAttempts++;
+ loadEventsReady = Services.telemetry.areUntrustedModuleLoadEventsReady;
+ }
+
+ data = await Services.telemetry.getUntrustedModuleLoadEvents(
+ Services.telemetry.INCLUDE_OLD_LOADEVENTS |
+ Services.telemetry.KEEP_LOADEVENTS_NEW |
+ Services.telemetry.INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS |
+ Services.telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS
+ );
+ } catch (e) {
+ // No error report in case of NS_ERROR_NOT_AVAILABLE
+ // because the method throws it when data is empty.
+ if (
+ !(e instanceof Components.Exception) ||
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ console.error(e);
+ }
+ }
+
+ if (!data || !data.modules || !data.processes) {
+ return null;
+ }
+
+ // The original telemetry data structure has an array of modules
+ // and an array of loading events referring to the module array's
+ // item via its index.
+ // To easily display data per module, we put loading events into
+ // a corresponding module object and return the module array.
+
+ for (const module of data.modules) {
+ module.events = [];
+ module.loadingOnMain = { count: 0, sum: 0 };
+
+ const moduleName = module.dllFile?.leafName;
+ module.typeFlags = AboutThirdParty.lookupModuleType(moduleName);
+ module.isCrasher = CrashModuleSet?.has(moduleName);
+
+ module.application = AboutThirdParty.lookupApplication(
+ module.dllFile?.path
+ );
+ module.moduleName = module.dllFile?.leafName;
+ module.hasLoadInformation = true;
+ }
+
+ let blockedModules = data.blockedModules.map(blockedModuleName => {
+ return {
+ moduleName: blockedModuleName,
+ typeFlags: AboutThirdParty.lookupModuleType(blockedModuleName),
+ isCrasher: CrashModuleSet?.has(blockedModuleName),
+ hasLoadInformation: false,
+ };
+ });
+
+ for (const [proc, perProc] of Object.entries(data.processes)) {
+ for (const event of perProc.events) {
+ // The expected format of |proc| is <type>.<pid> like "browser.0x1234"
+ const [ptype, pidHex] = proc.split(".");
+ event.processType = ptype;
+ event.processID = parseInt(pidHex, 16);
+
+ event.mainThread =
+ event.threadName == "MainThread" || event.threadName == "Main Thread";
+
+ const module = data.modules[event.moduleIndex];
+ if (event.mainThread) {
+ ++module.loadingOnMain.count;
+ module.loadingOnMain.sum += event.loadDurationMS;
+ }
+
+ module.events.push(event);
+ }
+ }
+
+ for (const module of data.modules) {
+ const avg = module.loadingOnMain.count
+ ? module.loadingOnMain.sum / module.loadingOnMain.count
+ : 0;
+ module.loadingOnMain = avg;
+ module.events.sort((a, b) => {
+ const diff = a.processType.localeCompare(b.processType);
+ return diff ? diff : a.processID - b.processID;
+ });
+ // If this module was blocked but not by the user, it must have been blocked
+ // by the static blocklist.
+ // But we don't know this for sure unless the background tasks were done
+ // by the time we gathered data about the module above.
+ if (gBackgroundTasksDone) {
+ module.isBlockedByBuiltin =
+ !(
+ module.typeFlags &
+ Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
+ ) &&
+ !!module.events.length &&
+ module.events.every(e => e.loadStatus !== 0);
+ } else {
+ module.isBlockedByBuiltin = false;
+ }
+ }
+
+ data.modules.sort(moduleCompareForDisplay);
+
+ return { modules: data.modules, blocked: blockedModules };
+}
+
+function setContent(element, text, l10n) {
+ if (text) {
+ element.textContent = text;
+ } else if (l10n) {
+ document.l10n.setAttributes(element, l10n);
+ }
+}
+
+function onClickOpenDir(event) {
+ const module = event.target.closest(".card").module;
+ if (!module?.dllFile) {
+ return;
+ }
+ module.dllFile.reveal();
+}
+
+// Returns whether we should restart.
+async function confirmRestartPrompt() {
+ let [msg, title, restartButtonText, restartLaterButtonText] =
+ await document.l10n.formatValues([
+ { id: "third-party-blocking-requires-restart" },
+ { id: "third-party-should-restart-title" },
+ { id: "third-party-restart-now" },
+ { id: "third-party-restart-later" },
+ ]);
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1_DEFAULT;
+ let buttonIndex = Services.prompt.confirmEx(
+ window.browsingContext.topChromeWindow,
+ title,
+ msg,
+ buttonFlags,
+ restartButtonText,
+ restartLaterButtonText,
+ null,
+ null,
+ {}
+ );
+ return buttonIndex === 0;
+}
+
+let processingBlockRequest = false;
+async function onBlock(event) {
+ const module = event.target.closest(".card").module;
+ if (!module?.moduleName) {
+ return;
+ }
+ // To avoid race conditions, don't allow any modules to be blocked/unblocked
+ // until we've updated and written the blocklist.
+ if (processingBlockRequest) {
+ return;
+ }
+ processingBlockRequest = true;
+
+ let updatedBlocklist = false;
+ try {
+ const wasBlocked = event.target.classList.contains("module-blocked");
+ await AboutThirdParty.updateBlocklist(module.moduleName, !wasBlocked);
+
+ event.target.classList.toggle("module-blocked");
+ let blockButtonL10nId;
+ if (wasBlocked) {
+ blockButtonL10nId = "third-party-button-to-block-module";
+ } else {
+ blockButtonL10nId = AboutThirdParty.isDynamicBlocklistDisabled
+ ? "third-party-button-to-unblock-module-disabled"
+ : "third-party-button-to-unblock-module";
+ }
+ document.l10n.setAttributes(event.target, blockButtonL10nId);
+ updatedBlocklist = true;
+ } catch (ex) {
+ console.error("Failed to update the blocklist file - ", ex.result);
+ } finally {
+ processingBlockRequest = false;
+ }
+ if (updatedBlocklist && (await confirmRestartPrompt())) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (!cancelQuit.data) {
+ // restart was not cancelled.
+ // Note that even if we're in safe mode, we don't restart
+ // into safe mode, because it's likely the user is trying to
+ // fix a crash or something, and they'd probably like to
+ // see if it works.
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ }
+ }
+}
+
+function onClickExpand(event) {
+ const card = event.target.closest(".card");
+ const button = event.target.closest("button");
+
+ const table = card.querySelector(".event-table");
+ if (!table) {
+ return;
+ }
+
+ if (table.hidden) {
+ table.hidden = false;
+ button.classList.add("button-collapse");
+ button.classList.remove("button-expand");
+ setContent(button, null, "third-party-button-collapse");
+ } else {
+ table.hidden = true;
+ button.classList.add("button-expand");
+ button.classList.remove("button-collapse");
+ setContent(button, null, "third-party-button-expand");
+ }
+}
+
+function createDetailRow(label, value) {
+ if (!document.templateDetailRow) {
+ document.templateDetailRow = document.querySelector(
+ "template[name=module-detail-row]"
+ );
+ }
+
+ const fragment = document.templateDetailRow.content.cloneNode(true);
+ setContent(fragment.querySelector("div > label"), null, label);
+ setContent(fragment.querySelector("div > span"), value);
+ return fragment;
+}
+
+function copyDataToClipboard(aData) {
+ const modulesData = aData.modules.map(module => {
+ const copied = {
+ name: module.moduleName,
+ fileVersion: module.fileVersion,
+ };
+
+ // We include the typeFlags field only when it's not 0 because
+ // typeFlags == 0 means system info is not yet collected.
+ if (module.typeFlags) {
+ copied.typeFlags = module.typeFlags;
+ }
+ if (module.signedBy) {
+ copied.signedBy = module.signedBy;
+ }
+ if (module.isCrasher) {
+ copied.isCrasher = module.isCrasher;
+ }
+ if (module.companyName) {
+ copied.companyName = module.companyName;
+ }
+ if (module.application) {
+ copied.applicationName = module.application.name;
+ copied.applicationPublisher = module.application.publisher;
+ }
+
+ if (Array.isArray(module.events)) {
+ copied.events = module.events.map(event => {
+ return {
+ processType: event.processType,
+ processID: event.processID,
+ threadID: event.threadID,
+ loadStatus: event.loadStatus,
+ loadDurationMS: event.loadDurationMS,
+ };
+ });
+ }
+
+ return copied;
+ });
+ const blockedData = aData.blocked.map(blockedModule => {
+ const copied = {
+ name: blockedModule.moduleName,
+ };
+ // We include the typeFlags field only when it's not 0 because
+ // typeFlags == 0 means system info is not yet collected.
+ if (blockedModule.typeFlags) {
+ copied.typeFlags = blockedModule.typeFlags;
+ }
+ if (blockedModule.isCrasher) {
+ copied.isCrasher = blockedModule.isCrasher;
+ }
+ return copied;
+ });
+ let clipboardData = { modules: modulesData, blocked: blockedData };
+
+ return navigator.clipboard.writeText(JSON.stringify(clipboardData, null, 2));
+}
+
+function correctProcessTypeForFluent(type) {
+ // GetProcessTypeString() in UntrustedModulesDataSerializer.cpp converted
+ // the "default" process type to "browser" to send as telemetry. We revert
+ // it to pass to ProcessType API.
+ const geckoType = type == "browser" ? "default" : type;
+ return ProcessType.fluentNameFromProcessTypeString(geckoType);
+}
+
+function setUpBlockButton(aCard, isBlocklistDisabled, aModule) {
+ const blockButton = aCard.querySelector(".button-block");
+ if (aModule.hasLoadInformation) {
+ if (!aModule.isBlockedByBuiltin) {
+ blockButton.hidden = aModule.typeFlags == 0;
+ }
+ } else {
+ // This means that this is an entry in the dynamic blocklist that
+ // has not attempted to load, thus we have very little information
+ // about it (just its name). So this should always show up.
+ blockButton.hidden = false;
+ // Bug 1808904 - don't allow unblocking this module before we've loaded
+ // the list of blocked modules in the background task.
+ blockButton.disabled = !gBackgroundTasksDone;
+ }
+ // If we haven't loaded the typeFlags yet and we don't have any load information for this
+ // module, default to showing that the module is blocked (because we must have gotten this
+ // module's info from the dynamic blocklist)
+ if (
+ aModule.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUser ||
+ (aModule.typeFlags == 0 && !aModule.hasLoadInformation)
+ ) {
+ blockButton.classList.add("module-blocked");
+ }
+
+ if (isBlocklistDisabled) {
+ blockButton.classList.add("blocklist-disabled");
+ }
+ if (blockButton.classList.contains("module-blocked")) {
+ document.l10n.setAttributes(
+ blockButton,
+ isBlocklistDisabled
+ ? "third-party-button-to-unblock-module-disabled"
+ : "third-party-button-to-unblock-module"
+ );
+ }
+}
+
+function visualizeData(aData) {
+ const templateCard = document.querySelector("template[name=card]");
+ const templateBlockedCard = document.querySelector(
+ "template[name=card-blocked]"
+ );
+ const templateTableRow = document.querySelector(
+ "template[name=event-table-row]"
+ );
+
+ // These correspond to the enum ModuleLoadInfo::Status
+ const labelLoadStatus = [
+ "third-party-status-loaded",
+ "third-party-status-blocked",
+ "third-party-status-redirected",
+ "third-party-status-blocked",
+ ];
+
+ const isBlocklistAvailable =
+ AboutThirdParty.isDynamicBlocklistAvailable &&
+ Services.policies.isAllowed("thirdPartyModuleBlocking");
+ const isBlocklistDisabled = AboutThirdParty.isDynamicBlocklistDisabled;
+
+ const mainContentFragment = new DocumentFragment();
+
+ // Blocklist entries are case-insensitive
+ let lowercaseModuleNames = new Set(
+ aData.modules.map(module => module.moduleName.toLowerCase())
+ );
+ for (const module of aData.blocked) {
+ if (lowercaseModuleNames.has(module.moduleName.toLowerCase())) {
+ // Only show entries that we haven't already tried to load,
+ // because those will already show up in the page
+ continue;
+ }
+ const newCard = templateBlockedCard.content.cloneNode(true);
+ setContent(newCard.querySelector(".module-name"), module.moduleName);
+ // Referred by the button click handlers
+ newCard.querySelector(".card").module = {
+ moduleName: module.moduleName,
+ };
+
+ if (isBlocklistAvailable) {
+ setUpBlockButton(newCard, isBlocklistDisabled, module);
+ }
+ if (module.isCrasher) {
+ newCard.querySelector(".image-warning").hidden = false;
+ }
+ mainContentFragment.appendChild(newCard);
+ }
+
+ for (const module of aData.modules) {
+ const newCard = templateCard.content.cloneNode(true);
+ const moduleName = module.moduleName;
+
+ // Referred by the button click handlers
+ newCard.querySelector(".card").module = {
+ dllFile: module.dllFile,
+ moduleName: module.moduleName,
+ fileVersion: module.fileVersion,
+ };
+
+ setContent(newCard.querySelector(".module-name"), moduleName);
+
+ const modTagsContainer = newCard.querySelector(".module-tags");
+ if (module.typeFlags & Ci.nsIAboutThirdParty.ModuleType_IME) {
+ modTagsContainer.querySelector(".tag-ime").hidden = false;
+ }
+ if (module.typeFlags & Ci.nsIAboutThirdParty.ModuleType_ShellExtension) {
+ modTagsContainer.querySelector(".tag-shellex").hidden = false;
+ }
+
+ newCard.querySelector(".blocked-by-builtin").hidden =
+ !module.isBlockedByBuiltin;
+ if (isBlocklistAvailable) {
+ setUpBlockButton(newCard, isBlocklistDisabled, module);
+ }
+
+ if (module.isCrasher) {
+ newCard.querySelector(".image-warning").hidden = false;
+ }
+
+ if (!module.signedBy) {
+ newCard.querySelector(".image-unsigned").hidden = false;
+ }
+
+ const modDetailContainer = newCard.querySelector(".module-details");
+
+ if (module.application) {
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-app", module.application.name)
+ );
+ modDetailContainer.appendChild(
+ createDetailRow(
+ "third-party-detail-publisher",
+ module.application.publisher
+ )
+ );
+ }
+
+ if (module.fileVersion) {
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-version", module.fileVersion)
+ );
+ }
+
+ const vendorInfo = module.signedBy || module.companyName;
+ if (vendorInfo) {
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-vendor", vendorInfo)
+ );
+ }
+
+ modDetailContainer.appendChild(
+ createDetailRow("third-party-detail-occurrences", module.events.length)
+ );
+ modDetailContainer.appendChild(
+ createDetailRow(
+ "third-party-detail-duration",
+ module.loadingOnMain || "-"
+ )
+ );
+
+ const eventTable = newCard.querySelector(".event-table > tbody");
+ for (const event of module.events) {
+ const fragment = templateTableRow.content.cloneNode(true);
+
+ const row = fragment.querySelector("tr");
+
+ setContent(
+ row.children[0].querySelector(".process-type"),
+ null,
+ correctProcessTypeForFluent(event.processType)
+ );
+ setContent(row.children[0].querySelector(".process-id"), event.processID);
+
+ // Use setContent() instead of simple assignment because
+ // loadDurationMS can be empty (not zero) when a module is
+ // loaded very early in the process and we need to show
+ // a text in that case.
+ setContent(
+ row.children[1].querySelector(".event-duration"),
+ event.loadDurationMS,
+ "third-party-message-no-duration"
+ );
+ row.querySelector(".tag-background").hidden = event.mainThread;
+
+ setContent(row.children[2], null, labelLoadStatus[event.loadStatus]);
+ eventTable.appendChild(fragment);
+ }
+
+ mainContentFragment.appendChild(newCard);
+ }
+
+ const main = document.getElementById("main");
+ main.appendChild(mainContentFragment);
+ main.addEventListener("click", onClickInMain);
+}
+
+function onClickInMain(event) {
+ const classList = event.target.classList;
+ if (classList.contains("button-open-dir")) {
+ onClickOpenDir(event);
+ } else if (classList.contains("button-block")) {
+ onBlock(event);
+ } else if (
+ classList.contains("button-expand") ||
+ classList.contains("button-collapse")
+ ) {
+ onClickExpand(event);
+ }
+}
+
+function clearVisualizedData() {
+ const mainDiv = document.getElementById("main");
+ while (mainDiv.firstChild) {
+ mainDiv.firstChild.remove();
+ }
+}
+
+async function collectCrashInfo() {
+ const parseBigInt = maybeBigInt => {
+ try {
+ return BigInt(maybeBigInt);
+ } catch (e) {
+ console.error(e);
+ }
+ return NaN;
+ };
+
+ if (CrashModuleSet || !AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ const { getCrashManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/CrashManager.sys.mjs"
+ );
+ const crashes = await getCrashManager().getCrashes();
+ CrashModuleSet = new Set(
+ crashes.map(crash => {
+ const stackInfo = crash.metadata?.StackTraces;
+ if (!stackInfo) {
+ return null;
+ }
+
+ const crashAddr = parseBigInt(stackInfo.crash_info?.address);
+ if (typeof crashAddr !== "bigint") {
+ return null;
+ }
+
+ // Find modules whose address range includes the crashing address.
+ // No need to check the type of the return value from parseBigInt
+ // because comparing BigInt with NaN returns false.
+ return stackInfo.modules?.find(
+ module =>
+ crashAddr >= parseBigInt(module.base_addr) &&
+ crashAddr < parseBigInt(module.end_addr)
+ )?.filename;
+ })
+ );
+}
+
+async function onLoad() {
+ document
+ .getElementById("button-copy-to-clipboard")
+ .addEventListener("click", async e => {
+ e.target.disabled = true;
+
+ const data = await fetchData();
+ await copyDataToClipboard(data || []).catch(console.error);
+
+ e.target.disabled = false;
+ });
+
+ const backgroundTasks = [
+ AboutThirdParty.collectSystemInfo(),
+ collectCrashInfo(),
+ ];
+
+ let hasData = false;
+ Promise.all(backgroundTasks)
+ .then(() => {
+ gBackgroundTasksDone = true;
+ // Reload button will either show or is not needed, so we can hide the
+ // loading indicator.
+ document.getElementById("background-data-loading").hidden = true;
+ if (!hasData) {
+ // If all async tasks were completed before fetchData,
+ // or there was no data available, visualizeData shows
+ // full info and the reload button is not needed.
+ return;
+ }
+
+ // Add {once: true} to prevent multiple listeners from being scheduled
+ const button = document.getElementById("button-reload");
+ button.addEventListener(
+ "click",
+ async event => {
+ // Update the content with data we've already collected.
+ clearVisualizedData();
+ visualizeData(await fetchData());
+ event.target.hidden = true;
+ },
+ { once: true }
+ );
+
+ // Coming here means visualizeData is completed before the background
+ // tasks are completed. Because the page does not show full information,
+ // we show the reload button to call visualizeData again.
+ button.hidden = false;
+ })
+ .catch(console.error);
+
+ const data = await fetchData();
+ // Used for testing purposes
+ window.fetchDataDone = true;
+
+ hasData = !!data?.modules.length || !!data?.blocked.length;
+ if (!hasData) {
+ document.getElementById("no-data").hidden = false;
+ return;
+ }
+
+ visualizeData(data);
+}
+
+try {
+ AboutThirdParty = Cc["@mozilla.org/about-thirdparty;1"].getService(
+ Ci.nsIAboutThirdParty
+ );
+ document.addEventListener("DOMContentLoaded", onLoad, { once: true });
+} catch (ex) {
+ // Do nothing if we fail to create a singleton instance,
+ // showing the default no-module message.
+ console.error(ex);
+}