protections.mjs (17452B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import LockwiseCard from "./lockwise-card.mjs"; 6 import MonitorCard from "./monitor-card.mjs"; 7 import ProxyCard from "./proxy-card.mjs"; 8 import VPNCard from "./vpn-card.mjs"; 9 10 let cbCategory = RPMGetStringPref("browser.contentblocking.category"); 11 document.sendTelemetryEvent = (eventName, value = "") => { 12 RPMRecordGleanEvent("securityUiProtections", eventName, { 13 value, 14 category: cbCategory, 15 }); 16 }; 17 18 let { protocol, pathname, searchParams } = new URL(document.location); 19 20 let searchParamsChanged = false; 21 if (searchParams.has("entrypoint")) { 22 RPMSendAsyncMessage("RecordEntryPoint", { 23 entrypoint: searchParams.get("entrypoint"), 24 }); 25 // Remove this parameter from the URL (after recording above) to make it 26 // cleaner for bookmarking and switch-to-tab and so that bookmarked values 27 // don't skew telemetry. 28 searchParams.delete("entrypoint"); 29 searchParamsChanged = true; 30 } 31 32 document.addEventListener("DOMContentLoaded", () => { 33 if (searchParamsChanged) { 34 let newURL = protocol + pathname; 35 let params = searchParams.toString(); 36 if (params) { 37 newURL += "?" + params; 38 } 39 window.location.replace(newURL); 40 return; 41 } 42 43 RPMSendQuery("FetchEntryPoint", {}).then(entrypoint => { 44 // Send telemetry on arriving on this page 45 document.sendTelemetryEvent("showProtectionReport", entrypoint); 46 }); 47 48 // We need to send the close telemetry before unload while we still have a connection to RPM. 49 window.addEventListener("beforeunload", () => { 50 document.sendTelemetryEvent("closeProtectionReport"); 51 }); 52 53 let todayInMs = Date.now(); 54 let weekAgoInMs = todayInMs - 6 * 24 * 60 * 60 * 1000; 55 56 let dataTypes = [ 57 "cryptominer", 58 "fingerprinter", 59 "tracker", 60 "cookie", 61 "social", 62 ]; 63 64 let manageProtectionsLink = document.getElementById("protection-settings"); 65 let manageProtections = document.getElementById("manage-protections"); 66 let protectionSettingsEvtHandler = evt => { 67 if (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click") { 68 RPMSendAsyncMessage("OpenContentBlockingPreferences"); 69 if (evt.target.id == "protection-settings") { 70 document.sendTelemetryEvent("clickSettingsLink", "header-settings"); 71 } else if (evt.target.id == "manage-protections") { 72 document.sendTelemetryEvent( 73 "clickSettingsLink", 74 "custom-card-settings" 75 ); 76 } 77 } 78 }; 79 manageProtectionsLink.addEventListener("click", protectionSettingsEvtHandler); 80 manageProtectionsLink.addEventListener( 81 "keypress", 82 protectionSettingsEvtHandler 83 ); 84 manageProtections.addEventListener("click", protectionSettingsEvtHandler); 85 manageProtections.addEventListener("keypress", protectionSettingsEvtHandler); 86 87 let legend = document.getElementById("legend"); 88 legend.style.gridTemplateAreas = 89 "'social cookie tracker fingerprinter cryptominer'"; 90 91 let createGraph = data => { 92 let graph = document.getElementById("graph"); 93 let summary = document.getElementById("graph-total-summary"); 94 let weekSummary = document.getElementById("graph-week-summary"); 95 96 // User is in private mode, show no data on the graph 97 if (data.isPrivate) { 98 graph.classList.add("private-window"); 99 } else { 100 let earliestDate = data.earliestDate || Date.now(); 101 document.l10n.setAttributes(summary, "graph-total-tracker-summary", { 102 count: data.sumEvents, 103 earliestDate, 104 }); 105 } 106 107 // Set a default top size for the height of the graph bars so that small 108 // numbers don't fill the whole graph. 109 let largest = 100; 110 if (largest < data.largest) { 111 largest = data.largest; 112 } 113 let weekCount = 0; 114 let weekTypeCounts = { 115 social: 0, 116 cookie: 0, 117 tracker: 0, 118 fingerprinter: 0, 119 cryptominer: 0, 120 }; 121 122 // For accessibility clients, we turn the graph into a fake table with annotated text. 123 // We use WAI-ARIA roles, properties, and states to mark up the table, rows and cells. 124 // Each day becomes one row in the table. 125 // Each row contains the day, total, and then one cell for each bar that we display. 126 // At most, a row can contain seven cells. 127 // But we need to caclulate the actual number of the most cells in a row to give accurate information. 128 let maxColumnCount = 0; 129 let date = new Date(); 130 for (let i = 0; i <= 6; i++) { 131 let dateString = date.toISOString().split("T")[0]; 132 let ariaOwnsString = ""; // Get the row's colummns in order 133 let currentColumnCount = 0; 134 let bar = document.createElement("div"); 135 bar.className = "graph-bar"; 136 bar.setAttribute("role", "row"); 137 let innerBar = document.createElement("div"); 138 innerBar.className = "graph-wrapper-bar"; 139 if (data[dateString]) { 140 let content = data[dateString]; 141 let count = document.createElement("div"); 142 count.className = "bar-count"; 143 count.id = "count" + i; 144 count.setAttribute("role", "cell"); 145 count.textContent = content.total; 146 setTimeout(() => { 147 count.classList.add("animate"); 148 }, 400); 149 bar.appendChild(count); 150 ariaOwnsString = count.id; 151 currentColumnCount += 1; 152 let barHeight = (content.total / largest) * 100; 153 weekCount += content.total; 154 // Add a short timeout to allow the elements to be added to the dom before triggering an animation. 155 setTimeout(() => { 156 bar.style.height = `${barHeight}%`; 157 }, 20); 158 for (let type of dataTypes) { 159 if (content[type]) { 160 let dataHeight = (content[type] / content.total) * 100; 161 // Since we are dealing with non-visual content, screen readers need a parent container to get the text 162 let cellSpan = document.createElement("span"); 163 cellSpan.id = type + i; 164 cellSpan.setAttribute("role", "cell"); 165 let div = document.createElement("div"); 166 div.className = `${type}-bar inner-bar`; 167 div.setAttribute("role", "img"); 168 div.setAttribute("data-type", type); 169 div.style.height = `${dataHeight}%`; 170 const messageIDs = { 171 social: "bar-tooltip-social", 172 cookie: "bar-tooltip-cookie", 173 tracker: "bar-tooltip-tracker", 174 cryptominer: "bar-tooltip-cryptominer", 175 fingerprinter: "bar-tooltip-fingerprinter", 176 }; 177 document.l10n.setAttributes(div, messageIDs[type], { 178 count: content[type], 179 percentage: dataHeight, 180 }); 181 weekTypeCounts[type] += content[type]; 182 cellSpan.appendChild(div); 183 innerBar.appendChild(cellSpan); 184 ariaOwnsString = ariaOwnsString + " " + cellSpan.id; 185 currentColumnCount += 1; 186 } 187 } 188 if (currentColumnCount > maxColumnCount) { 189 // The current row has more than any previous rows 190 maxColumnCount = currentColumnCount; 191 } 192 } else { 193 // There were no content blocking events on this day. 194 bar.classList.add("empty"); 195 } 196 bar.appendChild(innerBar); 197 graph.prepend(bar); 198 199 if (data.isPrivate) { 200 document.l10n.setAttributes( 201 weekSummary, 202 "graph-week-summary-private-window" 203 ); 204 } else { 205 document.l10n.setAttributes(weekSummary, "graph-week-summary", { 206 count: weekCount, 207 }); 208 } 209 210 let label = document.createElement("span"); 211 label.className = "column-label"; 212 // While the graphs fill up from the right, the days fill up from the left, so match the IDs 213 label.id = "day" + (6 - i); 214 label.setAttribute("role", "rowheader"); 215 if (i == 6) { 216 document.l10n.setAttributes(label, "graph-today"); 217 } else { 218 label.textContent = data.weekdays[(i + 1 + new Date().getDay()) % 7]; 219 } 220 graph.append(label); 221 // Make the day the first column in a row, making it the row header. 222 bar.setAttribute("aria-owns", "day" + i + " " + ariaOwnsString); 223 date.setDate(date.getDate() - 1); 224 } 225 maxColumnCount += 1; // Add the day column in the fake table 226 graph.setAttribute("aria-colCount", maxColumnCount); 227 // Set the total number of each type of tracker on the tabs as well as their 228 // "Learn More" links 229 for (let type of dataTypes) { 230 document.querySelector(`label[data-type=${type}] span`).textContent = 231 weekTypeCounts[type]; 232 const learnMoreLink = document.getElementById(`${type}-link`); 233 learnMoreLink.href = RPMGetFormatURLPref( 234 `browser.contentblocking.report.${type}.url` 235 ); 236 learnMoreLink.addEventListener("click", () => { 237 document.sendTelemetryEvent("clickTrackersAboutLink", type); 238 }); 239 } 240 241 let blockingCookies = 242 RPMGetIntPref("network.cookie.cookieBehavior", 0) != 0; 243 let cryptominingEnabled = RPMGetBoolPref( 244 "privacy.trackingprotection.cryptomining.enabled", 245 false 246 ); 247 let fingerprintingEnabled = 248 RPMGetBoolPref( 249 "privacy.trackingprotection.fingerprinting.enabled", 250 false 251 ) || RPMGetBoolPref("privacy.fingerprintingProtection", false); 252 let tpEnabled = RPMGetBoolPref("privacy.trackingprotection.enabled", false); 253 let socialTracking = RPMGetBoolPref( 254 "privacy.trackingprotection.socialtracking.enabled", 255 false 256 ); 257 let socialCookies = RPMGetBoolPref( 258 "privacy.socialtracking.block_cookies.enabled", 259 false 260 ); 261 let socialEnabled = 262 socialCookies && (blockingCookies || (tpEnabled && socialTracking)); 263 let notBlocking = 264 !blockingCookies && 265 !cryptominingEnabled && 266 !fingerprintingEnabled && 267 !tpEnabled && 268 !socialEnabled; 269 270 // User has turned off all blocking, show a different card. 271 if (notBlocking) { 272 document.l10n.setAttributes( 273 document.getElementById("etp-card-content"), 274 "protection-report-etp-card-content-custom-not-blocking" 275 ); 276 document.l10n.setAttributes( 277 document.querySelector(".etp-card .card-title"), 278 "etp-card-title-custom-not-blocking" 279 ); 280 document.l10n.setAttributes( 281 document.getElementById("report-summary"), 282 "protection-report-page-summary" 283 ); 284 document.querySelector(".etp-card").classList.add("custom-not-blocking"); 285 286 // Hide the link to settings from the header, so we are not showing two links. 287 manageProtectionsLink.style.display = "none"; 288 } else { 289 // Hide each type of tab if blocking of that type is off. 290 if (!tpEnabled) { 291 legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace( 292 "tracker", 293 "" 294 ); 295 let radio = document.getElementById("tab-tracker"); 296 radio.setAttribute("disabled", true); 297 document.querySelector("#tab-tracker ~ label").style.display = "none"; 298 } 299 if (!socialEnabled) { 300 legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace( 301 "social", 302 "" 303 ); 304 let radio = document.getElementById("tab-social"); 305 radio.setAttribute("disabled", true); 306 document.querySelector("#tab-social ~ label").style.display = "none"; 307 } 308 if (!blockingCookies) { 309 legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace( 310 "cookie", 311 "" 312 ); 313 let radio = document.getElementById("tab-cookie"); 314 radio.setAttribute("disabled", true); 315 document.querySelector("#tab-cookie ~ label").style.display = "none"; 316 } 317 if (!cryptominingEnabled) { 318 legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace( 319 "cryptominer", 320 "" 321 ); 322 let radio = document.getElementById("tab-cryptominer"); 323 radio.setAttribute("disabled", true); 324 document.querySelector("#tab-cryptominer ~ label").style.display = 325 "none"; 326 } 327 if (!fingerprintingEnabled) { 328 legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace( 329 "fingerprinter", 330 "" 331 ); 332 let radio = document.getElementById("tab-fingerprinter"); 333 radio.setAttribute("disabled", true); 334 document.querySelector("#tab-fingerprinter ~ label").style.display = 335 "none"; 336 } 337 338 let firstRadio = document.querySelector("input:enabled"); 339 // There will be no radio options if we are showing the 340 firstRadio.checked = true; 341 document.body.setAttribute("focuseddatatype", firstRadio.dataset.type); 342 343 addListeners(); 344 } 345 }; 346 347 let addListeners = () => { 348 let wrapper = document.querySelector(".body-wrapper"); 349 let triggerTabClick = ev => { 350 if (ev.originalTarget.dataset.type) { 351 document.getElementById(`tab-${ev.target.dataset.type}`).click(); 352 } 353 }; 354 355 let triggerTabFocus = ev => { 356 if (ev.originalTarget.dataset) { 357 wrapper.classList.add("hover-" + ev.originalTarget.dataset.type); 358 } 359 }; 360 361 let triggerTabBlur = ev => { 362 if (ev.originalTarget.dataset) { 363 wrapper.classList.remove("hover-" + ev.originalTarget.dataset.type); 364 } 365 }; 366 wrapper.addEventListener("mouseout", triggerTabBlur); 367 wrapper.addEventListener("mouseover", triggerTabFocus); 368 wrapper.addEventListener("click", triggerTabClick); 369 370 // Change the class on the body to change the color variable. 371 let radios = document.querySelectorAll("#legend input"); 372 for (let radio of radios) { 373 radio.addEventListener("change", ev => { 374 document.body.setAttribute("focuseddatatype", ev.target.dataset.type); 375 }); 376 radio.addEventListener("focus", ev => { 377 wrapper.classList.add("hover-" + ev.originalTarget.dataset.type); 378 document.body.setAttribute("focuseddatatype", ev.target.dataset.type); 379 }); 380 radio.addEventListener("blur", ev => { 381 wrapper.classList.remove("hover-" + ev.originalTarget.dataset.type); 382 }); 383 } 384 }; 385 386 RPMSendQuery("FetchContentBlockingEvents", { 387 from: weekAgoInMs, 388 to: todayInMs, 389 }).then(createGraph); 390 391 let exitIcon = document.querySelector("#mobile-hanger .exit-icon"); 392 // hide the mobile promotion and keep hidden with a pref. 393 exitIcon.addEventListener("click", () => { 394 RPMSetPref("browser.contentblocking.report.show_mobile_app", false); 395 document.getElementById("mobile-hanger").classList.add("hidden"); 396 }); 397 398 let androidMobileAppLink = document.getElementById( 399 "android-mobile-inline-link" 400 ); 401 androidMobileAppLink.href = RPMGetStringPref( 402 "browser.contentblocking.report.mobile-android.url" 403 ); 404 androidMobileAppLink.addEventListener("click", () => { 405 document.sendTelemetryEvent("clickMobileAppLink", "android"); 406 }); 407 let iosMobileAppLink = document.getElementById("ios-mobile-inline-link"); 408 iosMobileAppLink.href = RPMGetStringPref( 409 "browser.contentblocking.report.mobile-ios.url" 410 ); 411 iosMobileAppLink.addEventListener("click", () => { 412 document.sendTelemetryEvent("clickMobileAppLink", "ios"); 413 }); 414 415 let lockwiseEnabled = RPMGetBoolPref( 416 "browser.contentblocking.report.lockwise.enabled", 417 true 418 ); 419 420 let lockwiseCard; 421 if (lockwiseEnabled) { 422 const lockwiseUI = document.querySelector(".lockwise-card"); 423 lockwiseUI.classList.remove("hidden"); 424 lockwiseUI.classList.add("loading"); 425 426 lockwiseCard = new LockwiseCard(document); 427 lockwiseCard.init(); 428 } 429 430 RPMSendQuery("FetchUserLoginsData", {}).then(data => { 431 if (lockwiseCard) { 432 // Once data for the user is retrieved, display the lockwise card. 433 lockwiseCard.buildContent(data); 434 } 435 436 if ( 437 RPMGetBoolPref("browser.contentblocking.report.show_mobile_app") && 438 !data.mobileDeviceConnected 439 ) { 440 document 441 .getElementById("mobile-hanger") 442 .classList.toggle("hidden", false); 443 } 444 }); 445 446 // For tests 447 const lockwiseUI = document.querySelector(".lockwise-card"); 448 lockwiseUI.dataset.enabled = lockwiseEnabled; 449 450 let monitorEnabled = RPMGetBoolPref( 451 "browser.contentblocking.report.monitor.enabled", 452 true 453 ); 454 if (monitorEnabled) { 455 // Show the Monitor card. 456 const monitorUI = document.querySelector(".card.monitor-card.hidden"); 457 monitorUI.classList.remove("hidden"); 458 monitorUI.classList.add("loading"); 459 460 const monitorCard = new MonitorCard(document); 461 monitorCard.init(); 462 } 463 464 // For tests 465 const monitorUI = document.querySelector(".monitor-card"); 466 monitorUI.dataset.enabled = monitorEnabled; 467 468 const proxyEnabled = RPMGetBoolPref( 469 "browser.contentblocking.report.proxy.enabled", 470 true 471 ); 472 473 if (proxyEnabled) { 474 const proxyCard = new ProxyCard(document); 475 proxyCard.init(); 476 } 477 478 // For tests 479 const proxyUI = document.querySelector(".proxy-card"); 480 proxyUI.dataset.enabled = proxyEnabled; 481 482 const VPNEnabled = RPMGetBoolPref("browser.vpn_promo.enabled", true); 483 if (VPNEnabled) { 484 const vpnCard = new VPNCard(document); 485 vpnCard.init(); 486 } 487 // For tests 488 const vpnUI = document.querySelector(".vpn-card"); 489 vpnUI.dataset.enabled = VPNEnabled; 490 });