tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });