/** \file * \brief Functions that summarize fingerprints and generate FPD report * \ingroup FPD * * \author Copyright (C) 2022 Marek Salon * * \license SPDX-License-Identifier: GPL-3.0-or-later */ // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // let hiddenTraces = {}; /** * Event listener that listens for a load of FPD report page. If the page is loaded, fetch FPD data from background. * * \param callback Function that initializes FPD report creation from fetched data. */ window.addEventListener('load', () => { browser.runtime.sendMessage({ purpose: "fpd-get-report-data", tabId: new URLSearchParams(window.location.search).get("id") }).then((result) => { createReport(result); }); }); /** * The function that populates FPD report page with data from the latest evaluation and hooks up listeners. * * \param data Information about latest fingerprinting evaluation consisting of all essential FPD objects. */ function createReport(data) { var {tabObj, groups, latestEvals, fpDb, exceptionWrappers} = data; var reportContainer = document.getElementById("fpd-report-container"); if (!tabObj || !groups || !groups.root || !groups.all || !fpDb || !latestEvals) { report.innerHTML =`
${browser.i18n.getMessage("FPDReportMissingData")}
`; return; } // parse latestEvals to create more useful representation for FPD report generation var processedEvals = {}; for (let item of latestEvals.evalStats) { processedEvals[item.title] = processedEvals[item.title] || {}; processedEvals[item.title].type = item.type; let total = 0; if (fpDb[item.title]) { for (let stat of Object.values(fpDb[item.title])) { total += stat.total; } } processedEvals[item.title].total = total; } // add page URL and FavIcon to header section of the report if (tabObj) { let urlObj = new URL(tabObj.url); var url = urlObj.hostname + (urlObj.pathname.length > 1 ? urlObj.pathname : ""); document.getElementById("report-url").innerHTML = url; let img = document.getElementById("pageFavicon"); img.src = tabObj.favIconUrl; } var html = ""; // generate html code for evaluated group let generateGroup = (group) => { if (processedEvals[group]) { if (groups.all[group].description) { html += "
"; html += "

" + group + "

"; html += ``; html += "
"; html += "

" + groups.all[group].description + "

"; } for (let [item, type] of Object.entries(groups.all[group].items)) { if (type == "group") { generateGroup(item); } else { generateResource(item) } } if (groups.all[group].description) { html += "
"; } } } // generate html code for evaluated resource (get,set,call) let generateResource = (resource) => { if (processedEvals[resource]) { let callers = ""; if (fpDb[resource]) { for (let t of Object.values(fpDb[resource])) { let traces = Object.keys(t.callers); for (trace of traces) { if (trace !== "" && !(trace in hiddenTraces)) { callers += "

" + trace.replace(/\n/g, '
') + "

"; } } } } let accessRaw = processedEvals[resource].total; let accessCount = accessRaw >= 1000 ? "1000+" : accessRaw; html += `

- ` + `${resource} (${exceptionWrappers.includes(resource) ? "n/a" : accessCount})

\n${callers}
`; } } // start generating FPD report from the first group (root group) generateGroup(groups.root); var report = document.getElementById("fpd-report"); report.innerHTML = html; // process generated document let groupElements = document.querySelectorAll(".fpd-group.access"); for (let i = groupElements.length; i > 0; i--) { // remove duplicit entries from groups let duplicitArray = []; let elements = groupElements[i-1].querySelectorAll(":scope > section > div.details > h4"); elements.forEach((d) => { if (duplicitArray.indexOf(d.innerHTML) > -1) { d.remove(); } else { duplicitArray.push(d.innerHTML); } }); // hide groups with no relevant entries if (!document.querySelectorAll(`#${groupElements[i-1].id} > section > .access`).length) { groupElements[i-1].classList.replace("access", "no-access"); } } // function that enables to show accessed resources of the group function toggleResources(event) { let parent = event.target.parentElement; for (let i = 0; i < parent.children.length; i++) { let child = parent.children[i]; if (child.tagName == "SECTION") { child.classList.toggle("hidden"); } } } // make group name clickable only if it makes sense - groups with resources function makeGroupExpansionsClickable() { for (let element of document.querySelectorAll(".fpd-group")) { let button; for (let i = 0; i < element.children.length; i++) { let child = element.children[i]; if (child.tagName == "BUTTON") { button = child; } } if (button) { button.classList.add("clickable"); button.addEventListener("click", toggleResources); } } } makeGroupExpansionsClickable(); // show resources for every group in FPD report let showAll = (event) => { for (let element of document.querySelectorAll(".fpd-group div.details")) { element.classList.remove("hidden"); } showBtn.classList.add("hidden"); hideBtn.classList.remove("hidden"); } // hide resources for every group in FPD report let hideDetails = (event) => { for (let element of document.querySelectorAll(".fpd-group div.details")) { element.classList.add("hidden"); } showBtn.classList.remove("hidden"); hideBtn.classList.add("hidden"); } hideDetails(); // refresh data in the report function refreshReport() { browser.runtime.sendMessage({ purpose: "fpd-get-report-data", tabId: tabId }).then((result) => { createReport(result); showAll(); }); browser.runtime.sendMessage({purpose: "fpd-track-callers-stop"}); document.getElementById("updateReportBtn").classList.remove("hidden"); document.getElementById("forgetCurrentBtn").classList.remove("hidden"); let trackCallersBtn = document.getElementById("trackCallersBtn"); trackCallersBtn.innerText = browser.i18n.getMessage("FPDReportTrackCallersRestart"); trackCallersBtn.classList.remove("hidden"); } // Reload the report with data on the identity of the calling scripts function trackCallers() { tabId = new URLSearchParams(window.location.search).get("id"); function onReloaded() { setTimeout(refreshReport, 5000); report.innerHTML = browser.i18n.getMessage("FPDReportTrackCallersWaiting"); document.getElementById("trackCallersBtn").classList.add("hidden"); document.getElementById("updateReportBtn").classList.add("hidden"); document.getElementById("forgetCurrentBtn").classList.add("hidden"); } function onError(error) { document.getElementById("fpdError").innerHTML = browser.i18n.getMessage("FPDReportTrackCallersFailed", error); browser.runtime.sendMessage({purpose: "fpd-track-callers-stop"}); } browser.runtime.sendMessage({ purpose: "fpd-track-callers", tabId: tabId }).then(onReloaded, onError); } function updateReport() { } // show all groups/resources even if not accessed let showNotAccessed = () => { for (let element of document.querySelectorAll(".no-access")) { element.classList.remove("no-access"); } makeGroupExpansionsClickable(); } function forgetTraces() { for (resource of Object.values(fpDb)) { for (type of Object.values(resource)) { for (trace of Object.keys(type.callers)) { hiddenTraces[trace] = true; } } } createReport(data); } // create on-site JSON representation of FPD evaluation data and download it function exportReport(filename) { let element = document.createElement("a"); let obj = { fpd_evaluation_statistics: latestEvals.evalStats, fpd_access_logs: fpDb } element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(obj))); element.setAttribute("download", filename); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); } document.getElementById("showBtn").onclick = showAll; document.getElementById("hideBtn").onclick = hideDetails; document.getElementById("exportBtn").onclick = exportReport.bind(null, `fpd_report_${url}.json`); document.getElementById("trackCallersBtn").onclick = trackCallers; document.getElementById("forgetCurrentBtn").onclick = forgetTraces; document.getElementById("updateReportBtn").onclick = refreshReport; document.getElementById("unhideAll").onclick = showNotAccessed; } // show description/help for the report let showDescription = () => { for (let element of document.querySelectorAll(".description")) { element.classList.toggle("hidden"); } } window.addEventListener("DOMContentLoaded", function() { document.getElementById("titletext").innerHTML += ''; document.getElementById("help").addEventListener("click", showDescription); });