summaryrefslogtreecommitdiffstats
path: root/browser/components/protections
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/protections
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/protections')
-rw-r--r--browser/components/protections/content/lockwise-card.mjs142
-rw-r--r--browser/components/protections/content/monitor-card.mjs449
-rw-r--r--browser/components/protections/content/protections.css1127
-rw-r--r--browser/components/protections/content/protections.ftl26
-rw-r--r--browser/components/protections/content/protections.html597
-rw-r--r--browser/components/protections/content/protections.mjs490
-rw-r--r--browser/components/protections/content/proxy-card.mjs29
-rw-r--r--browser/components/protections/content/vpn-card.mjs103
-rw-r--r--browser/components/protections/jar.mn12
-rw-r--r--browser/components/protections/moz.build12
-rw-r--r--browser/components/protections/test/browser/browser.toml18
-rw-r--r--browser/components/protections/test/browser/browser_protections_lockwise.js290
-rw-r--r--browser/components/protections/test/browser/browser_protections_monitor.js161
-rw-r--r--browser/components/protections/test/browser/browser_protections_proxy.js107
-rw-r--r--browser/components/protections/test/browser/browser_protections_report_ui.js1129
-rw-r--r--browser/components/protections/test/browser/browser_protections_telemetry.js1123
-rw-r--r--browser/components/protections/test/browser/browser_protections_vpn.js282
-rw-r--r--browser/components/protections/test/browser/head.js96
18 files changed, 6193 insertions, 0 deletions
diff --git a/browser/components/protections/content/lockwise-card.mjs b/browser/components/protections/content/lockwise-card.mjs
new file mode 100644
index 0000000000..cc25d4351b
--- /dev/null
+++ b/browser/components/protections/content/lockwise-card.mjs
@@ -0,0 +1,142 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+const HOW_IT_WORKS_URL_PREF = RPMGetFormatURLPref(
+ "browser.contentblocking.report.lockwise.how_it_works.url"
+);
+
+export default class LockwiseCard {
+ constructor(doc) {
+ this.doc = doc;
+ }
+
+ /**
+ * Initializes message listeners/senders.
+ */
+ init() {
+ const savePasswordsButton = this.doc.getElementById(
+ "save-passwords-button"
+ );
+ savePasswordsButton.addEventListener(
+ "click",
+ this.openAboutLogins.bind(this)
+ );
+
+ const managePasswordsButton = this.doc.getElementById(
+ "manage-passwords-button"
+ );
+ managePasswordsButton.addEventListener(
+ "click",
+ this.openAboutLogins.bind(this)
+ );
+
+ // Attack link to Firefox Lockwise "How it works" page.
+ const lockwiseReportLink = this.doc.getElementById("lockwise-how-it-works");
+ lockwiseReportLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "lw_about_link");
+ });
+ }
+
+ openAboutLogins() {
+ const lockwiseCard = this.doc.querySelector(".lockwise-card");
+ if (lockwiseCard.classList.contains("has-logins")) {
+ if (lockwiseCard.classList.contains("breached-logins")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "lw_open_button",
+ "manage_breached_passwords"
+ );
+ } else if (lockwiseCard.classList.contains("no-breached-logins")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "lw_open_button",
+ "manage_passwords"
+ );
+ }
+ } else if (lockwiseCard.classList.contains("no-logins")) {
+ this.doc.sendTelemetryEvent("click", "lw_open_button", "save_passwords");
+ }
+ RPMSendAsyncMessage("OpenAboutLogins");
+ }
+
+ buildContent(data) {
+ const { numLogins, potentiallyBreachedLogins } = data;
+ const hasLogins = numLogins > 0;
+ const title = this.doc.getElementById("lockwise-title");
+ const headerContent = this.doc.querySelector(
+ "#lockwise-header-content span"
+ );
+ const lockwiseCard = this.doc.querySelector(".card.lockwise-card");
+
+ if (hasLogins) {
+ lockwiseCard.classList.remove("no-logins");
+ lockwiseCard.classList.add("has-logins");
+ document.l10n.setAttributes(title, "passwords-title-logged-in");
+ document.l10n.setAttributes(
+ headerContent,
+ "lockwise-header-content-logged-in"
+ );
+ this.renderContentForLoggedInUser(numLogins, potentiallyBreachedLogins);
+ } else {
+ lockwiseCard.classList.remove("has-logins");
+ lockwiseCard.classList.add("no-logins");
+ document.l10n.setAttributes(title, "lockwise-title");
+ document.l10n.setAttributes(headerContent, "passwords-header-content");
+ }
+
+ const lockwiseUI = document.querySelector(".card.lockwise-card.loading");
+ lockwiseUI.classList.remove("loading");
+ }
+
+ /**
+ * Displays strings indicating stored logins for a user.
+ *
+ * @param {number} storedLogins
+ * The number of browser-stored logins.
+ * @param {number} potentiallyBreachedLogins
+ * The number of potentially breached logins.
+ */
+ renderContentForLoggedInUser(storedLogins, potentiallyBreachedLogins) {
+ const lockwiseScannedText = this.doc.getElementById(
+ "lockwise-scanned-text"
+ );
+ const lockwiseScannedIcon = this.doc.getElementById(
+ "lockwise-scanned-icon"
+ );
+ const lockwiseCard = this.doc.querySelector(".card.lockwise-card");
+
+ if (potentiallyBreachedLogins) {
+ document.l10n.setAttributes(
+ lockwiseScannedText,
+ "lockwise-scanned-text-breached-logins",
+ {
+ count: potentiallyBreachedLogins,
+ }
+ );
+ lockwiseScannedIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/breached-password.svg"
+ );
+ lockwiseCard.classList.add("breached-logins");
+ } else {
+ document.l10n.setAttributes(
+ lockwiseScannedText,
+ "lockwise-scanned-text-no-breached-logins",
+ {
+ count: storedLogins,
+ }
+ );
+ lockwiseScannedIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/resolved-breach.svg"
+ );
+ lockwiseCard.classList.add("no-breached-logins");
+ }
+
+ const howItWorksLink = this.doc.getElementById("lockwise-how-it-works");
+ howItWorksLink.href = HOW_IT_WORKS_URL_PREF;
+ }
+}
diff --git a/browser/components/protections/content/monitor-card.mjs b/browser/components/protections/content/monitor-card.mjs
new file mode 100644
index 0000000000..ef693a761e
--- /dev/null
+++ b/browser/components/protections/content/monitor-card.mjs
@@ -0,0 +1,449 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+const MONITOR_URL = RPMGetStringPref(
+ "browser.contentblocking.report.monitor.url",
+ ""
+);
+const MONITOR_SIGN_IN_URL = RPMGetStringPref(
+ "browser.contentblocking.report.monitor.sign_in_url",
+ ""
+);
+const HOW_IT_WORKS_URL_PREF = RPMGetFormatURLPref(
+ "browser.contentblocking.report.monitor.how_it_works.url"
+);
+const MONITOR_PREFERENCES_URL = RPMGetFormatURLPref(
+ "browser.contentblocking.report.monitor.preferences_url"
+);
+const MONITOR_HOME_PAGE_URL = RPMGetFormatURLPref(
+ "browser.contentblocking.report.monitor.home_page_url"
+);
+
+export default class MonitorClass {
+ constructor(doc) {
+ this.doc = doc;
+ }
+
+ init() {
+ // Wait for monitor data and display the card.
+ this.getMonitorData();
+
+ let monitorAboutLink = this.doc.getElementById("monitor-link");
+ monitorAboutLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "mtr_about_link");
+ });
+
+ const storedEmailLink = this.doc.getElementById(
+ "monitor-stored-emails-link"
+ );
+ storedEmailLink.href = MONITOR_PREFERENCES_URL;
+ storedEmailLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+
+ const knownBreachesLink = this.doc.getElementById(
+ "monitor-known-breaches-link"
+ );
+ knownBreachesLink.href = MONITOR_HOME_PAGE_URL;
+ knownBreachesLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+
+ const exposedPasswordsLink = this.doc.getElementById(
+ "monitor-exposed-passwords-link"
+ );
+ exposedPasswordsLink.href = MONITOR_HOME_PAGE_URL;
+ exposedPasswordsLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+ }
+
+ onClickMonitorButton(evt) {
+ RPMSendAsyncMessage("ClearMonitorCache");
+ switch (evt.currentTarget.id) {
+ case "monitor-partial-breaches-link":
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "resolve_breaches"
+ );
+ break;
+ case "monitor-breaches-link":
+ if (evt.currentTarget.classList.contains("no-breaches-resolved")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "manage_breaches"
+ );
+ } else {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "view_report"
+ );
+ }
+ break;
+ case "monitor-stored-emails-link":
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "stored_emails"
+ );
+ break;
+ case "monitor-known-breaches-link":
+ const knownBreaches = this.doc.querySelector(
+ "span[data-type='known-breaches']"
+ );
+ if (knownBreaches.classList.contains("known-resolved-breaches")) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "known_resolved_breaches"
+ );
+ } else if (
+ knownBreaches.classList.contains("known-unresolved-breaches")
+ ) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "known_unresolved_breaches"
+ );
+ }
+ break;
+ case "monitor-exposed-passwords-link":
+ const exposedPasswords = this.doc.querySelector(
+ "span[data-type='exposed-passwords']"
+ );
+ if (
+ exposedPasswords.classList.contains("passwords-exposed-all-breaches")
+ ) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "exposed_passwords_all_breaches"
+ );
+ } else if (
+ exposedPasswords.classList.contains(
+ "passwords-exposed-unresolved-breaches"
+ )
+ ) {
+ this.doc.sendTelemetryEvent(
+ "click",
+ "mtr_report_link",
+ "exposed_passwords_unresolved_breaches"
+ );
+ }
+ break;
+ }
+ }
+
+ /**
+ * Retrieves the monitor data and displays this data in the card.
+ */
+ getMonitorData() {
+ RPMSendQuery("FetchMonitorData", {}).then(monitorData => {
+ // Once data for the user is retrieved, display the monitor card.
+ this.buildContent(monitorData);
+
+ // Show the Monitor card.
+ const monitorUI = this.doc.querySelector(".card.monitor-card.loading");
+ monitorUI.classList.remove("loading");
+ });
+ }
+
+ buildContent(monitorData) {
+ const headerContent = this.doc.querySelector(
+ "#monitor-header-content span"
+ );
+ const monitorCard = this.doc.querySelector(".card.monitor-card");
+ if (!monitorData.error) {
+ monitorCard.classList.add("has-logins");
+ this.doc.l10n.setAttributes(
+ headerContent,
+ "monitor-header-content-signed-in"
+ );
+ this.renderContentForUserWithAccount(monitorData);
+ } else {
+ monitorCard.classList.add("no-logins");
+ const signUpForMonitorLink = this.doc.getElementById(
+ "sign-up-for-monitor-link"
+ );
+ signUpForMonitorLink.href = this.buildMonitorUrl(monitorData.userEmail);
+ this.doc.l10n.setAttributes(signUpForMonitorLink, "monitor-sign-up-link");
+ this.doc.l10n.setAttributes(
+ headerContent,
+ "monitor-header-content-no-account"
+ );
+ signUpForMonitorLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "mtr_signup_button");
+ });
+ }
+ }
+
+ /**
+ * Builds the appropriate URL that takes the user to the Monitor website's
+ * sign-up/sign-in page.
+ *
+ * @param {string | null} email
+ * Optional. The email used to direct the user to the Monitor website's OAuth
+ * sign-in flow. If null, then direct user to just the Monitor website.
+ *
+ * @returns {string} URL to Monitor website.
+ */
+ buildMonitorUrl(email = null) {
+ return email
+ ? `${MONITOR_SIGN_IN_URL}${encodeURIComponent(email)}`
+ : MONITOR_URL;
+ }
+
+ renderContentForUserWithAccount(monitorData) {
+ const {
+ numBreaches,
+ numBreachesResolved,
+ passwords,
+ passwordsResolved,
+ monitoredEmails,
+ } = monitorData;
+ const monitorCardBody = this.doc.querySelector(
+ ".card.monitor-card .card-body"
+ );
+ monitorCardBody.classList.remove("hidden");
+
+ const howItWorksLink = this.doc.getElementById("monitor-link");
+ howItWorksLink.href = HOW_IT_WORKS_URL_PREF;
+
+ const storedEmail = this.doc.querySelector(
+ "span[data-type='stored-emails']"
+ );
+ storedEmail.textContent = monitoredEmails;
+ const infoMonitoredAddresses = this.doc.getElementById(
+ "info-monitored-addresses"
+ );
+ this.doc.l10n.setAttributes(
+ infoMonitoredAddresses,
+ "info-monitored-emails",
+ { count: monitoredEmails }
+ );
+
+ const knownBreaches = this.doc.querySelector(
+ "span[data-type='known-breaches']"
+ );
+ const exposedPasswords = this.doc.querySelector(
+ "span[data-type='exposed-passwords']"
+ );
+
+ const infoKnownBreaches = this.doc.getElementById("info-known-breaches");
+ const infoExposedPasswords = this.doc.getElementById(
+ "info-exposed-passwords"
+ );
+
+ const breachesWrapper = this.doc.querySelector(".monitor-breaches-wrapper");
+ const partialBreachesWrapper = this.doc.querySelector(
+ ".monitor-partial-breaches-wrapper"
+ );
+ const breachesTitle = this.doc.getElementById("monitor-breaches-title");
+ const breachesIcon = this.doc.getElementById("monitor-breaches-icon");
+ const breachesDesc = this.doc.getElementById(
+ "monitor-breaches-description"
+ );
+ const breachesLink = this.doc.getElementById("monitor-breaches-link");
+ if (numBreaches) {
+ if (!numBreachesResolved) {
+ partialBreachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreaches;
+ knownBreaches.classList.add("known-unresolved-breaches");
+ knownBreaches.classList.remove("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-found",
+ { count: numBreaches }
+ );
+ exposedPasswords.textContent = passwords;
+ exposedPasswords.classList.add("passwords-exposed-all-breaches");
+ exposedPasswords.classList.remove(
+ "passwords-exposed-unresolved-breaches"
+ );
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-found",
+ { count: passwords }
+ );
+
+ breachesIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/new-feature.svg"
+ );
+ this.doc.l10n.setAttributes(
+ breachesTitle,
+ "monitor-breaches-unresolved-title"
+ );
+ this.doc.l10n.setAttributes(
+ breachesDesc,
+ "monitor-breaches-unresolved-description"
+ );
+ this.doc.l10n.setAttributes(
+ breachesLink,
+ "monitor-manage-breaches-link"
+ );
+ breachesLink.classList.add("no-breaches-resolved");
+ } else if (numBreaches == numBreachesResolved) {
+ partialBreachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreachesResolved;
+ knownBreaches.classList.remove("known-unresolved-breaches");
+ knownBreaches.classList.add("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-resolved",
+ { count: numBreachesResolved }
+ );
+ let unresolvedPasswords = passwords - passwordsResolved;
+ exposedPasswords.textContent = unresolvedPasswords;
+ exposedPasswords.classList.remove("passwords-exposed-all-breaches");
+ exposedPasswords.classList.add("passwords-exposed-unresolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-resolved",
+ { count: unresolvedPasswords }
+ );
+
+ breachesIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/resolved-breach.svg"
+ );
+ this.doc.l10n.setAttributes(
+ breachesTitle,
+ "monitor-breaches-resolved-title"
+ );
+ this.doc.l10n.setAttributes(
+ breachesDesc,
+ "monitor-breaches-resolved-description"
+ );
+ this.doc.l10n.setAttributes(breachesLink, "monitor-view-report-link");
+ } else {
+ breachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreachesResolved;
+ knownBreaches.classList.remove("known-unresolved-breaches");
+ knownBreaches.classList.add("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-resolved",
+ { count: numBreachesResolved }
+ );
+ let unresolvedPasswords = passwords - passwordsResolved;
+ exposedPasswords.textContent = unresolvedPasswords;
+ exposedPasswords.classList.remove("passwords-exposed-all-breaches");
+ exposedPasswords.classList.add("passwords-exposed-unresolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-resolved",
+ { count: unresolvedPasswords }
+ );
+
+ const partialBreachesTitle = document.getElementById(
+ "monitor-partial-breaches-title"
+ );
+ this.doc.l10n.setAttributes(
+ partialBreachesTitle,
+ "monitor-partial-breaches-title",
+ {
+ numBreaches,
+ numBreachesResolved,
+ }
+ );
+
+ const progressBar = this.doc.querySelector(".progress-bar");
+ const partialBreachesMotivationTitle = document.getElementById(
+ "monitor-partial-breaches-motivation-title"
+ );
+
+ let percentageResolved = Math.floor(
+ (numBreachesResolved / numBreaches) * 100
+ );
+ progressBar.setAttribute("value", 100 - percentageResolved);
+ switch (true) {
+ case percentageResolved > 0 && percentageResolved < 25:
+ this.doc.l10n.setAttributes(
+ partialBreachesMotivationTitle,
+ "monitor-partial-breaches-motivation-title-start"
+ );
+ break;
+
+ case percentageResolved >= 25 && percentageResolved < 75:
+ this.doc.l10n.setAttributes(
+ partialBreachesMotivationTitle,
+ "monitor-partial-breaches-motivation-title-middle"
+ );
+ break;
+
+ case percentageResolved >= 75 && percentageResolved < 100:
+ this.doc.l10n.setAttributes(
+ partialBreachesMotivationTitle,
+ "monitor-partial-breaches-motivation-title-end"
+ );
+ break;
+ }
+
+ const partialBreachesPercentage = document.getElementById(
+ "monitor-partial-breaches-percentage"
+ );
+ this.doc.l10n.setAttributes(
+ partialBreachesPercentage,
+ "monitor-partial-breaches-percentage",
+ { percentageResolved }
+ );
+
+ const partialBreachesLink = document.getElementById(
+ "monitor-partial-breaches-link"
+ );
+ partialBreachesLink.setAttribute("href", MONITOR_HOME_PAGE_URL);
+ partialBreachesLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+ }
+ } else {
+ partialBreachesWrapper.classList.add("hidden");
+ knownBreaches.textContent = numBreaches;
+ knownBreaches.classList.add("known-unresolved-breaches");
+ knownBreaches.classList.remove("known-resolved-breaches");
+ this.doc.l10n.setAttributes(
+ infoKnownBreaches,
+ "info-known-breaches-found",
+ { count: numBreaches }
+ );
+ exposedPasswords.textContent = passwords;
+ exposedPasswords.classList.add("passwords-exposed-all-breaches");
+ exposedPasswords.classList.remove(
+ "passwords-exposed-unresolved-breaches"
+ );
+ this.doc.l10n.setAttributes(
+ infoExposedPasswords,
+ "info-exposed-passwords-found",
+ { count: passwords }
+ );
+
+ breachesIcon.setAttribute(
+ "src",
+ "chrome://browser/skin/protections/resolved-breach.svg"
+ );
+ this.doc.l10n.setAttributes(breachesTitle, "monitor-no-breaches-title");
+ this.doc.l10n.setAttributes(
+ breachesDesc,
+ "monitor-no-breaches-description"
+ );
+ this.doc.l10n.setAttributes(breachesLink, "monitor-view-report-link");
+ }
+
+ breachesLink.setAttribute("href", MONITOR_HOME_PAGE_URL);
+ breachesLink.addEventListener(
+ "click",
+ this.onClickMonitorButton.bind(this)
+ );
+ }
+}
diff --git a/browser/components/protections/content/protections.css b/browser/components/protections/content/protections.css
new file mode 100644
index 0000000000..de0ee0b2a0
--- /dev/null
+++ b/browser/components/protections/content/protections.css
@@ -0,0 +1,1127 @@
+/* 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/. */
+
+:root {
+ --card-padding: 24px;
+ --exit-icon-size: 12px;
+ --exit-icon-position: calc((var(--card-padding) - var(--exit-icon-size)) / 2);
+ --social-color: #9059FF;
+ --cookie-color: #0090F4;
+ --tracker-color: #2AC3A2;
+ --fingerprinter-color: #FFA436;
+ --cryptominer-color: #ADADBC;
+
+ /* Highlight colors for trackers */
+ --social-highlight-color: #7B4CDB;
+ --cookie-highlight-color: #0081DB;
+ --tracker-highlight-color: #23A488;
+ --fingerprinter-highlight-color: #D37F17;
+ --cryptominer-highlight-color: #9292A0;
+
+ --tab-highlight: var(--social-color); /* start with social selected */
+ --column-width: 16px;
+ --graph-empty: #CECECF;
+ --graph-curve: cubic-bezier(.66,.75,.59,.91);
+
+ /* Colors for the loading indicator */
+ --protection-report-loader-color-stop: #AEAEAE3D;
+ --protection-report-loader-gradient-opacity: 0.95;
+
+ --grey-70: #38383D;
+ --grey-90-a60: rgba(12, 12, 13, 0.6);
+
+ --gear-icon-fill: var(--grey-90-a60);
+ --hover-grey-link: var(--grey-70);
+ --feature-banner-color: rgba(0, 0, 0, 0.05);
+}
+
+body {
+ box-sizing: border-box;
+}
+
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
+body[focuseddatatype=social] {
+ --tab-highlight: var(--social-color);
+}
+
+body[focuseddatatype=cookie] {
+ --tab-highlight: var(--cookie-color);
+}
+
+body[focuseddatatype=tracker] {
+ --tab-highlight: var(--tracker-color);
+}
+
+body[focuseddatatype=fingerprinter] {
+ --tab-highlight: var(--fingerprinter-color);
+}
+
+body[focuseddatatype=cryptominer] {
+ --tab-highlight: var(--cryptominer-color);
+}
+
+h2 {
+ font-weight: var(--font-weight-bold);
+}
+
+#report-title {
+ margin-block-end: 20px;
+}
+
+#report-summary {
+ color: var(--text-color-deemphasized);
+ line-height: 26px;
+ font-size: 16px;
+ margin: 0;
+ margin-bottom: 15px;
+}
+
+#report-content {
+ width: 763px;
+ margin: 0 auto;
+ margin-block: 40px 80px;
+}
+
+.card-header .wrapper,
+.new-banner .wrapper {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ align-items: center;
+}
+
+#manage-protections,
+.card-header > button,
+#save-passwords-button,
+#get-proxy-extension-link,
+#get-vpn-link,
+#vpn-banner-link,
+#manage-passwords-button,
+#sign-up-for-monitor-link {
+ grid-area: 1 / 5 / 1 / -1;
+ margin: 0;
+ font-size: 0.95em;
+ cursor: pointer;
+ padding: 10px;
+ text-align: center;
+ line-height: initial;
+}
+
+#vpn-banner-link {
+ grid-area: 1 / 6 / 1 / -1;
+}
+
+.new-banner .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 6;
+ padding-inline-end: 15px;
+}
+
+.lockwise-card.has-logins .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 6;
+}
+
+.card:not(.has-logins) .wrapper div:nth-child(1),
+.etp-card.custom-not-blocking .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 5;
+ padding-inline-end: 15px;
+}
+
+.etp-card:not(.custom-not-blocking) .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / 8;
+}
+
+.card.has-logins .wrapper div:nth-child(1) {
+ grid-area: 1 / 1 / 1 / -1;
+}
+
+.vpn-card.subscribed .wrapper div:nth-child(1) {
+ padding-inline-end: 29px;
+ grid-area: 1 / 1 / 1 / 7;
+}
+
+/* We want to hide certain components depending on its state. */
+.no-logins .monitor-scanned-wrapper,
+.etp-card.custom-not-blocking .card-body,
+.etp-card.custom-not-blocking #protection-settings,
+#manage-protections,
+.etp-card .icon.dark,
+.proxy-card .icon.dark,
+.vpn-card .icon.dark,
+.vpn-banner .icon.dark,
+a.hidden,
+.loading .card-body,
+.lockwise-card.hidden,
+#lockwise-body-content .has-logins.hidden,
+#lockwise-body-content .no-logins.hidden,
+.lockwise-card.no-logins #lockwise-how-it-works,
+.lockwise-card.no-logins .lockwise-scanned-wrapper,
+.lockwise-card.no-logins #manage-passwords-button,
+.lockwise-card.has-logins #save-passwords-button,
+.monitor-card.hidden,
+.monitor-card.no-logins .card-body,
+.monitor-card.no-logins #monitor-header-content a,
+.monitor-card.no-logins .monitor-scanned-text,
+.monitor-card.no-logins .icon-small,
+.monitor-card.no-logins .monitor-breaches-wrapper,
+.monitor-card.no-logins .monitor-partial-breaches-wrapper,
+.monitor-card .monitor-breaches-wrapper.hidden,
+.monitor-card .monitor-partial-breaches-wrapper.hidden,
+.monitor-card.has-logins #sign-up-for-monitor-link,
+.loading a,
+.loading button,
+.loading .wrapper,
+.proxy-card.hidden,
+.vpn-card.hidden,
+.card-body.hidden,
+.hidden {
+ display: none;
+}
+
+.icon {
+ width: 64px;
+ height: 64px;
+ grid-column: 1;
+ margin: 0 auto;
+ z-index: 1;
+}
+
+.vpn-card .icon {
+ width: 56px;
+ height: 56px;
+}
+
+.new-banner .icon {
+ width: 50px;
+ height: 50px;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --social-highlight-color: #9661FF;
+ --cookie-highlight-color: #2BA8FF;
+ --tracker-highlight-color: #39E1BC;
+ --fingerprinter-highlight-color: #FFB65E;
+ --cryptominer-highlight-color: #BEBECA;
+
+ --gear-icon-fill: rgba(249, 249, 250, 0.60);
+ --hover-grey-link: var(--grey-30);
+ --feature-banner-color: rgba(255, 255, 255, 0.1);
+ }
+
+ .etp-card .icon.dark,
+ .proxy-card .icon.dark,
+ .vpn-card .icon.dark,
+ .vpn-banner .icon.dark {
+ display: block;
+ }
+
+ .etp-card .icon.light,
+ .proxy-card .icon.light,
+ .vpn-card .icon.light,
+ .vpn-banner .icon.light {
+ display: none;
+ }
+}
+
+.card {
+ display: grid;
+ grid-template-columns: 100%;
+ grid-template-rows: 20% auto;
+ padding: 0;
+ margin-block-end: 25px;
+ line-height: 1.3em;
+}
+
+.card-header,
+.card-body {
+ display: grid;
+ grid-template-columns: 1fr 7fr;
+ padding: var(--card-padding);
+ grid-gap: var(--card-padding);
+}
+
+.card .card-title {
+ font-size: inherit;
+ line-height: 1.25;
+ margin: 0;
+ cursor: default;
+}
+
+.card .content {
+ margin-block: 5px 0;
+ font-size: .93em;
+ cursor: default;
+ color: var(--text-color-deemphasized);
+}
+
+.exit-icon {
+ position: absolute;
+ width: var(--exit-icon-size);
+ height: var(--exit-icon-size);
+ top: var(--exit-icon-position);
+ inset-inline-end: var(--exit-icon-position);
+ background-image: url(chrome://global/skin/icons/close.svg);
+ background-size: calc(var(--exit-icon-size) - 2px);
+ background-color: transparent;
+ background-position: center;
+ background-repeat: no-repeat;
+ -moz-context-properties: fill;
+ fill: currentColor;
+
+ /* Override margin, padding, min-height and min-width set in common-shared.css */
+ padding: 0;
+ margin: 0;
+ min-width: 1px;
+ min-height: 1px;
+}
+
+.custom-not-blocking .content {
+ margin-block-end: 5px;
+}
+
+.custom-not-blocking #manage-protections {
+ display: initial;
+}
+
+#protection-settings {
+ -moz-context-properties: fill;
+ fill: var(--gear-icon-fill);
+ background: url("chrome://global/skin/icons/settings.svg") no-repeat 0;
+ cursor: pointer;
+ width: max-content;
+ color: var(--text-color-deemphasized);
+ margin-block: 6px 0;
+ font-size: 0.8em;
+ padding-block: 4px;
+ padding-inline: 24px 4px;
+}
+
+#protection-settings:dir(rtl) {
+ background-position-x: right;
+}
+
+#protection-settings:hover,
+#protection-settings:focus {
+ text-decoration: underline;
+ color: var(--hover-grey-link);
+ fill: var(--hover-grey-link);
+}
+
+#protection-settings:hover:active {
+ text-decoration: none;
+ color: var(--in-content-text-color);
+ fill: var(--in-content-text-color);
+}
+
+#protection-settings:-moz-focusring {
+ outline: none;
+}
+
+.card .card-body {
+ border-block-start: 1px solid var(--in-content-border-color);
+ grid-column: span 2;
+ grid-row: 2;
+ position: relative;
+}
+
+.body-wrapper {
+ grid-column: 2;
+}
+
+#graph-week-summary,
+#graph-total-summary {
+ font-size: 0.8em;
+}
+
+#graph-week-summary {
+ font-weight: bold;
+}
+
+#graph-wrapper {
+ width: 100%;
+ margin-block: 33px 25px;
+}
+
+#graph {
+ display: grid;
+ grid: repeat(10, 1fr) max-content / repeat(7, 1fr);
+ height: 130px;
+ margin-block-end: 10px;
+}
+
+#private-window-message {
+ border: 1px solid var(--in-content-border-color);
+ grid-area: 1 / 2 / 1 / 7;
+ background-color: var(--in-content-box-background-odd);
+ padding: 13px 45px;
+ font-size: 13px;
+ margin-bottom: 25px;
+ text-align: center;
+}
+
+#graph:not(.private-window) #private-window-message {
+ display: none;
+}
+
+/* Graph Bars */
+.graph-bar {
+ grid-row: 2 / -2;
+ align-self: flex-end;
+ width: var(--column-width);
+ position: relative;
+ height: 0;
+ transition: height 500ms var(--graph-curve);
+}
+
+.graph-wrapper-bar {
+ height: 100%;
+ width: 100%;
+ border-radius: 2px;
+ overflow: hidden;
+ outline: 1px solid transparent;
+}
+
+.graph-bar:hover {
+ cursor: pointer;
+}
+
+.graph-bar.empty {
+ height: 0;
+ border: 1px var(--graph-empty) solid;
+ border-radius: 4px;
+ cursor: default;
+}
+
+.social-bar {
+ background-color: var(--social-color);
+}
+
+.hover-social .social-bar {
+ background-color: var(--social-highlight-color);
+}
+
+.cookie-bar {
+ background-color: var(--cookie-color);
+}
+
+.hover-cookie .cookie-bar {
+ background-color: var(--cookie-highlight-color);
+}
+
+.tracker-bar {
+ background-color: var(--tracker-color);
+}
+
+.hover-tracker .tracker-bar {
+ background-color: var(--tracker-highlight-color);
+}
+
+.fingerprinter-bar {
+ background-color: var(--fingerprinter-color);
+}
+
+.hover-fingerprinter .fingerprinter-bar {
+ background-color: var(--fingerprinter-highlight-color);
+}
+
+.cryptominer-bar {
+ background-color: var(--cryptominer-color);
+}
+
+.hover-cryptominer .cryptominer-bar {
+ background-color: var(--cryptominer-highlight-color);
+}
+
+.column-label {
+ margin-block-start: 5px;
+ font-size: 0.9em;
+ width: var(--column-width);
+ grid-row: -1;
+}
+
+.bar-count {
+ position: absolute;
+ top: -21px;
+ font-size: 0.8em;
+ opacity: 0;
+ transition: opacity 700ms;
+ pointer-events: none;
+}
+
+.bar-count.animate {
+ opacity: 1;
+}
+
+/* Legend */
+#graphLegendDescription {
+ position: absolute;
+ opacity: 0;
+ z-index: -1;
+}
+
+input[type="radio"] {
+ position: absolute;
+ inset-inline-start: -100px;
+}
+
+#legend input:focus + label {
+ outline: solid 1px;
+ outline-offset: -1px;
+ outline-color: var(--tab-highlight);
+}
+
+#legend {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-template-rows: auto auto;
+ grid-gap: 0;
+ position: relative;
+ overflow: hidden;
+}
+
+#highlight {
+ background: var(--tab-highlight);
+ position: absolute;
+ height: 3px;
+ width: 100%;
+ align-self: end;
+ grid-column: 1 / span 1;
+ grid-row: 1 / 1;
+}
+
+#highlight-hover {
+ position: absolute;
+ height: 4px;
+ width: 100%;
+ bottom: -1px;
+ align-self: end;
+}
+
+#legend label {
+ margin-block-end: -1px;
+ padding: 15px 0;
+ padding-inline-end: 5px;
+ border: 3px solid transparent;
+ -moz-context-properties: fill;
+ display: inline-block;
+}
+
+#legend label:-moz-focusring {
+ outline: none;
+}
+
+.icon-small {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ margin-inline-end: 2px;
+}
+
+label span {
+ margin-inline-start: 1px;
+ display: inline-block;
+}
+
+label[data-type="social"] .icon-small {
+ fill: var(--social-color);
+}
+
+label[data-type="cookie"] .icon-small {
+ fill: var(--cookie-color);
+}
+
+label[data-type="tracker"] .icon-small {
+ fill: var(--tracker-color);
+}
+
+label[data-type="fingerprinter"] .icon-small {
+ fill: var(--fingerprinter-color);
+}
+
+label[data-type="cryptominer"] .icon-small {
+ fill: var(--cryptominer-color);
+}
+
+.hover-social label[for="tab-social"],
+.hover-cookie label[for="tab-cookie"],
+.hover-tracker label[for="tab-tracker"],
+.hover-fingerprinter label[for="tab-fingerprinter"],
+.hover-cryptominer label[for="tab-cryptominer"],
+label:hover {
+ cursor: pointer;
+}
+
+.tab-content {
+ display: none;
+ padding: 22px 20px 20px;
+ border-block-start: 1px solid var(--tab-highlight);
+ background-color: var(--in-content-box-background-odd);
+ font-size: 0.8em;
+ line-height: 1.75;
+}
+
+.tab-content .content-title {
+ font-weight: bold;
+}
+
+.tab-content p {
+ margin: 0;
+}
+
+.tab-content p:nth-of-type(2) {
+ color: var(--text-color-deemphasized);
+}
+
+#tab-social:checked ~ #social,
+#tab-cookie:checked ~ #cookie,
+#tab-tracker:checked ~ #tracker,
+#tab-fingerprinter:checked ~ #fingerprinter,
+#tab-cryptominer:checked ~ #cryptominer {
+ display: block;
+ grid-column: 1/ -1;
+ grid-row: 2;
+}
+
+input[data-type="social"]:checked ~ #highlight,
+.hover-social label[for="tab-social"] ~ #highlight-hover,
+label[for="tab-social"]:hover ~ #highlight-hover {
+ background-color: var(--social-color);
+ grid-area: social;
+}
+
+input[data-type="cookie"]:checked ~ #highlight,
+.hover-cookie label[for="tab-cookie"] ~ #highlight-hover,
+label[for="tab-cookie"]:hover ~ #highlight-hover {
+ background-color: var(--cookie-color);
+ grid-area: cookie;
+}
+
+input[data-type="tracker"]:checked ~ #highlight,
+.hover-tracker label[for="tab-tracker"] ~ #highlight-hover,
+label[for="tab-tracker"]:hover ~ #highlight-hover {
+ background-color: var(--tracker-color);
+ grid-area: tracker;
+}
+
+input[data-type="fingerprinter"]:checked ~ #highlight,
+.hover-fingerprinter label[for="tab-fingerprinter"] ~ #highlight-hover,
+label[for="tab-fingerprinter"]:hover ~ #highlight-hover {
+ background-color: var(--fingerprinter-color);
+ grid-area: fingerprinter;
+}
+
+input[data-type="cryptominer"]:checked ~ #highlight,
+.hover-cryptominer label[for="tab-cryptominer"] ~ #highlight-hover,
+label[for="tab-cryptominer"]:hover ~ #highlight-hover {
+ background-color: var(--cryptominer-color);
+ grid-area: cryptominer;
+}
+
+#mobile-hanger {
+ grid-column: 1;
+ grid-row: 3;
+}
+
+.etp-card {
+ margin-top: 31px;
+ grid-template-rows: 20% auto auto;
+}
+
+/* Lockwise Card */
+
+#lockwise-body-content > .no-logins,
+#lockwise-body-content > .has-logins,
+#etp-mobile-content {
+ display: grid;
+ font-size: 0.875em;
+ align-items: center;
+}
+
+#lockwise-body-content > .no-logins,
+#etp-mobile-content {
+ grid: 1fr / 1fr 6fr;
+}
+
+#lockwise-body-content > .has-logins {
+ grid: 1fr 1fr / minmax(70px, auto) 1fr;
+ grid-gap: 10px;
+}
+
+.mobile-app-icon {
+ height: 56px;
+ width: auto;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+#lockwise-app-links,
+#mobile-app-links {
+ display: block;
+}
+
+.block {
+ background-color: var(--grey-60);
+ border-radius: 4px;
+ text-align: center;
+ font-size: 1.125em;
+ font-weight: bold;
+ color: #fff;
+ padding: 7px;
+ line-height: 18px;
+}
+
+#lockwise-body-content .has-logins a {
+ margin-inline-start: 10px;
+}
+
+.lockwise-scanned-wrapper {
+ display: grid;
+ grid-template-columns: 24px auto;
+ margin-block-start: 24px;
+ grid-area: 2 / 1 / 2 / 5;
+ padding-bottom: 1.7em;
+}
+
+#lockwise-scanned-text {
+ margin-inline-end: 15px;
+}
+
+#lockwise-scanned-icon {
+ margin-top: 5px;
+}
+
+#manage-passwords-button {
+ grid-area: 2 / 5 / 2 / 7;
+ margin-inline-end: 15px;
+}
+
+.vpn-card.subscribed #get-vpn-link {
+ display: none;
+}
+
+.vpn-card:not(.subscribed) .content.subscribed {
+ display: none;
+}
+
+.vpn-card.subscribed .content:not(.subscribed) {
+ display: none;
+}
+
+/* Monitor card */
+.monitor-info-wrapper {
+ display: grid;
+ grid: 1fr / 1fr 1fr 1fr;
+ grid-column-start: 1;
+ grid-column-end: 7;
+}
+
+.monitor-scanned-wrapper {
+ margin-block-start: 24px;
+ font-size: 0.85em;
+ display: block;
+}
+
+.monitor-breaches-wrapper {
+ display: grid;
+ grid-area: 2 / 1 / 2 / 8;
+ grid: 1fr auto / repeat(7, 1fr);
+ margin-bottom: 24px;
+}
+
+.monitor-partial-breaches-wrapper {
+ display: grid;
+ grid-area: 2 / 1 / 2 / 8;
+ grid-template-columns: repeat(7, 1fr);
+ margin-block: 24px;
+}
+
+.monitor-partial-breaches-header {
+ grid-area: 1 / 1 / 1 / 7;
+ margin-inline-end: 15px;
+ margin-block: 6px;
+}
+
+#monitor-partial-breaches-percentage {
+ font-size: .93em;
+ cursor: default;
+ color: var(--text-color-deemphasized);
+ float: inline-end;
+}
+
+.progress-bar {
+ grid-area: 2 / 1 / 2 / 7;
+ margin-inline-start: 15px;
+ border-radius: 4px;
+ height: 25px;
+ box-shadow: 0 0 0 1px rgba(202, 201, 213, 0.5);
+ border: none;
+ background: linear-gradient(-45deg, #0250BB 0%, #9059FF 100%);
+ direction: rtl;
+}
+
+.progress-bar:dir(rtl) {
+ direction: ltr;
+ background: linear-gradient(-45deg, #0250BB 0%, #9059FF 100%);
+}
+
+.progress-bar::-moz-progress-bar {
+ background: #FFFFFF;
+ border-radius: 0 4px 4px 0;
+}
+
+.monitor-partial-breaches-motivation-text {
+ grid-template-columns: repeat(7, 1fr);
+ grid-area: 3 / 1 / 3 / 8;
+ margin-top: 25px;
+ display: grid;
+}
+
+.monitor-partial-breaches-motivation-wrapper {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-area: 2 / 1 / 2 / 8;
+}
+
+#monitor-partial-breaches-motivation-title {
+ font-weight: 700;
+ grid-area: 1 / 1 / 1 / 7;
+ margin-inline-end: 15px;
+}
+
+#monitor-breaches-description,
+#monitor-partial-breaches-motivation-desc {
+ grid-area: 1 / 1 / 1 / 5;
+ margin-block: auto;
+ margin-inline-end: 15px;
+}
+
+.monitor-breaches-header {
+ margin-top: 30px;
+ grid-area: 1 / 1 / 1 / 8;
+}
+
+.monitor-breaches-description-wrapper {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ grid-area: 2 / 1 / 2 / 8;
+}
+
+#monitor-partial-breaches-icon,
+#monitor-breaches-icon {
+ vertical-align: middle;
+ margin-inline-end: 2px;
+}
+
+#monitor-partial-breaches-title {
+ font-size: 0.93em;
+}
+
+#monitor-breaches-title {
+ font-weight: 700;
+}
+
+#monitor-breaches-title,
+#monitor-partial-breaches-title,
+#monitor-partial-breaches-motivation-title {
+ cursor: default;
+}
+
+.monitor-partial-breaches-link-wrapper,
+.monitor-breaches-link-wrapper {
+ margin-block: auto;
+ grid-area: 1 / 5 / 1 / 7;
+ margin-inline: 0 15px;
+ font-size: 0.95em;
+ text-align: center;
+ display: flex;
+}
+
+#monitor-breaches-link,
+#monitor-partial-breaches-link {
+ color: inherit;
+ outline: none;
+ text-decoration: none;
+ width: 157.267px;
+ padding: 9px;
+}
+
+.lockwise-card #lockwise-header-content > a,
+.monitor-card #monitor-header-content > a {
+ display: block;
+ margin-block-start: 5px;
+ width: max-content;
+}
+
+.monitor-card.has-logins #monitor-body-content {
+ display: grid;
+ grid: 1fr auto / repeat(7, 1fr);
+ align-items: center;
+}
+
+.monitor-card .card-body {
+ padding-top: 0;
+ border-block-start: none;
+}
+
+.monitor-block {
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ text-align: center;
+ margin-inline-end: 15px;
+}
+
+.monitor-block a {
+ outline: none;
+ color: #FFFFFF;
+ padding: 19px 0;
+}
+
+.monitor-block a:hover {
+ text-decoration: none;
+ color: #FFFFFF;
+}
+
+.email {
+ background: linear-gradient(162.33deg, #AB71FF 0%, #9059FF 100%);
+ grid-column: 1;
+}
+
+.email:hover {
+ background: linear-gradient(162.33deg, #7D43D1 0%, #7740E6 100%);
+}
+
+.email:dir(rtl) {
+ background: linear-gradient(197.67deg, #AB71FF 0%, #9059FF 100%);
+}
+
+.email:dir(rtl):hover {
+ background: linear-gradient(197.67deg, #7D43D1 0%, #7740E6 100%);
+}
+
+.breaches {
+ background: linear-gradient(162.33deg, #9059FF 0%, #7542E5 100%);
+ grid-column: 2;
+}
+
+.breaches:hover {
+ background: linear-gradient(162.33deg, #7740E6 0%, #4714B7 100%);
+}
+
+.breaches:dir(rtl) {
+ background: linear-gradient(197.67deg, #9059FF 0%, #7542E5 100%)
+}
+
+.breaches:dir(rtl):hover {
+ background: linear-gradient(197.67deg, #7740E6 0%, #4714B7 100%)
+}
+
+.passwords {
+ background: linear-gradient(162.33deg, #7542E5 0%, #592ACB 100%);
+ grid-column: 3;
+}
+
+.passwords:hover {
+ background: linear-gradient(162.33deg, #4714B7 0%, #2B009D 100%);
+}
+
+.passwords:dir(rtl) {
+ background: linear-gradient(197.67deg, #7542E5 0%, #592ACB 100%)
+}
+
+.passwords:dir(rtl):hover {
+ background: linear-gradient(197.67deg, #4714B7 0%, #2B009D 100%)
+}
+
+.monitor-stat {
+ display: flex;
+ font-size: 1.75em;
+ font-weight: bold;
+ margin-block-end: 5px;
+ word-break: break-all;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.monitor-icon {
+ margin-inline-end: 3px;
+}
+
+.icon-med {
+ width: 24px;
+ height: 24px;
+ -moz-context-properties: fill,fill-opacity;
+ fill: white;
+ fill-opacity: 0.65;
+ padding: 5px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.info-text {
+ font-size: 0.75em;
+ line-height: 13px;
+ margin: 0 10px;
+ display: inline-block;
+}
+
+.number-of-breaches.block {
+ font-size: 1.45em;
+ background-color: var(--orange-50);
+ padding: 15px;
+ grid-column: 1 / 2;
+ width: 70px;
+ height: 48px;
+}
+
+#manage-protections,
+#sign-up-for-monitor-link,
+#get-proxy-extension-link,
+#get-vpn-link,
+#vpn-banner-link,
+.monitor-partial-breaches-link-wrapper,
+.monitor-breaches-link-wrapper {
+ background-color: var(--in-content-primary-button-background);
+ border: 1px solid var(--in-content-primary-button-border-color);
+ border-radius: 4px;
+ text-decoration: none;
+ color: var(--in-content-primary-button-text-color);
+ font-weight: 600;
+}
+
+#manage-protections:hover,
+#sign-up-for-monitor-link:hover,
+#get-proxy-extension-link:hover,
+#get-vpn-link:hover,
+#vpn-banner-link:hover,
+#monitor-partial-breaches-link:hover,
+#monitor-breaches-link:hover {
+ background-color: var(--in-content-primary-button-background-hover);
+ color: var(--in-content-primary-button-text-color-hover);
+ border-color: var(--in-content-button-border-color-hover);
+}
+
+#manage-protections:hover:active,
+#sign-up-for-monitor-link:hover:active,
+#get-proxy-extension-link:hover:active,
+#get-vpn-link:hover:active,
+#vpn-banner-link:hover:active,
+#monitor-partial-breaches-link:hover:active,
+#monitor-breaches-link:hover:active {
+ background-color: var(--in-content-primary-button-background-active);
+ color: var(--in-content-primary-button-text-color-active);
+ border-color: var(--in-content-button-border-color-active);
+}
+
+#manage-protections:focus-visible,
+#sign-up-for-monitor-link:focus-visible,
+#get-proxy-extension-link:focus-visible,
+#get-vpn-link:focus-visible,
+#vpn-banner-link:focus-visible,
+#monitor-partial-breaches-link:focus-visible,
+.monitor-block > a:focus-visible,
+#monitor-breaches-link:focus-visible {
+ outline: var(--in-content-focus-outline);
+ outline-offset: var(--in-content-focus-outline-offset);
+}
+
+.monitor-card.loading::after,
+.lockwise-card.loading::after {
+ position: absolute;
+ height: 110px;
+ width: 765px;
+ content: "";
+ background-image: linear-gradient(to right, var(--in-content-box-background) 0%, var(--protection-report-loader-color-stop) 30%, var(--in-content-box-background) 40%, var(--in-content-box-background) 100%);
+ background-repeat: no-repeat;
+ animation-duration: 2s;
+ animation-iteration-count: infinite;
+ animation-name: loading;
+ animation-timing-function: cubic-bezier(.07,.95,1,1);
+ background-size: 700px 100%;
+ opacity: var(--protection-report-loader-gradient-opacity);
+}
+
+.monitor-card.loading:dir(rtl)::after,
+.lockwise-card.loading:dir(rtl)::after {
+ background-image: linear-gradient(to left, var(--in-content-box-background) 0%, var(--protection-report-loader-color-stop) 30%, var(--in-content-box-background) 40%, var(--in-content-box-background) 100%);
+ animation-name: loading-rtl;
+}
+
+@keyframes loading {
+ 0% {
+ background-position-x: -300px;
+ }
+
+ 100% {
+ background-position-x: 700px;
+ opacity: 0.02;
+ }
+}
+
+@keyframes loading-rtl {
+ 0% {
+ background-position-x: right -300px;
+ }
+
+ 100% {
+ background-position-x: right 700px;
+ opacity: 0.02;
+ }
+}
+
+.new-banner {
+ width: 100%;
+ background: var(--feature-banner-color);
+}
+
+.banner-wrapper {
+ width: 763px;
+ display: grid;
+ grid-template-columns: 1fr 7fr;
+ grid-gap: var(--card-padding);
+ line-height: 1.3em;
+ margin: 0 auto;
+ padding: 12px var(--card-padding);
+}
+
+.new-banner .banner-title {
+ margin: 0;
+ line-height: 1.25;
+ cursor: default;
+ font-size: inherit;
+}
+
+.new-banner .content {
+ margin-block: 5px 0;
+ font-size: 0.88em;
+ cursor: default;
+ color: var(--text-color-deemphasized);
+}
+
+.new-banner .exit-icon {
+ top: auto;
+ inset-inline-end: 30px;
+}
+
+.vpn-card .title-wrapper {
+ display: grid;
+ grid-template-columns: 24px auto;
+}
+
+.vpn-card:not(.subscribed) .card-title {
+ grid-area: 1 / 1 / 1 / -1;
+}
+
+.vpn-card.subscribed .card-title {
+ margin-inline-start: 3px;
+}
+
+.vpn-card:not(.subscribed) #check-icon {
+ display: none;
+}
diff --git a/browser/components/protections/content/protections.ftl b/browser/components/protections/content/protections.ftl
new file mode 100644
index 0000000000..0f5b96e7ff
--- /dev/null
+++ b/browser/components/protections/content/protections.ftl
@@ -0,0 +1,26 @@
+# 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/.
+
+### This file is not in a locales directory to prevent it from
+### being translated as the feature is still in heavy development
+### and strings are likely to change often.
+
+-secure-proxy-brand-name = Firefox Private Network
+
+proxy-title = Stay safe on public Wi-Fi
+proxy-header-content = { -secure-proxy-brand-name } makes wireless hotspots more secure to protect you from hackers.
+get-proxy-extension-link = Get the extension
+
+vpn-title = Take privacy protections beyond the browser
+vpn-header-content = Protect your entire device with { -mozilla-vpn-brand-name }. One tap encrypts all traffic and hides your location.
+get-vpn-link = Get { -mozilla-vpn-brand-name }
+
+vpn-title-subscribed = VPN: Subscribed
+# Note This text is not being translated, and the <br> will need to be removed if or when it does get translated
+vpn-header-content-subscribed = Using the { -mozilla-vpn-brand-name } encrypts all your traffic and hides your location — on up to 5 devices. Get the most from your subscription — add it from <br> the <a data-l10n-name="vpn-google-playstore-link">Google Play Store</a> or <a data-l10n-name="vpn-app-store-link">Apple App Store</a>.
+
+vpn-banner-header = Protection that extends beyond the browser
+# Note This text is not being translated, and the <br> will need to be removed if or when it does get translated
+vpn-banner-content = Try { -mozilla-vpn-brand-name } risk-free and see why TechRadar says, <br> “its speed, simplicity and low monthly price make it worth a look.”
+vpn-banner-link = Get { -mozilla-vpn-brand-name }
diff --git a/browser/components/protections/content/protections.html b/browser/components/protections/content/protections.html
new file mode 100644
index 0000000000..1374c30fd7
--- /dev/null
+++ b/browser/components/protections/content/protections.html
@@ -0,0 +1,597 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome: blob:; object-src 'none'"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="browser/protections.ftl" />
+ <link rel="localization" href="toolkit/branding/accounts.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <!-- Temporary "en-US"-only l10n strings -->
+ <link rel="localization" href="preview/protections.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link rel="stylesheet" href="chrome://browser/content/protections.css" />
+ <link rel="icon" href="chrome://browser/skin/controlcenter/dashboard.svg" />
+ <script
+ type="module"
+ src="chrome://browser/content/protections.mjs"
+ ></script>
+ <title data-l10n-id="protection-report-webpage-title"></title>
+ </head>
+
+ <body>
+ <div class="new-banner vpn-banner hidden">
+ <div class="banner-wrapper">
+ <img
+ class="icon light"
+ src="chrome://browser/content/logos/vpn-light.svg"
+ />
+ <img
+ class="icon dark"
+ src="chrome://browser/content/logos/vpn-dark.svg"
+ />
+ <div class="wrapper">
+ <div>
+ <h3 class="banner-title" data-l10n-id="vpn-banner-header"></h3>
+ <span class="content" data-l10n-id="vpn-banner-content"></span>
+ </div>
+ <a
+ target="_blank"
+ id="vpn-banner-link"
+ data-l10n-id="get-vpn-link"
+ ></a>
+ <button
+ class="exit-icon"
+ data-l10n-id="protections-close-button2"
+ ></button>
+ </div>
+ </div>
+ </div>
+ <div id="report-content">
+ <h1
+ id="report-title"
+ data-l10n-id="protection-report-page-content-title"
+ ></h1>
+ <p
+ id="report-summary"
+ data-l10n-id="protection-report-page-summary-default"
+ ></p>
+ <p
+ id="protection-settings"
+ data-l10n-id="protection-report-settings-link"
+ role="link"
+ tabindex="0"
+ ></p>
+ <div class="card card-no-hover etp-card">
+ <div class="card-header">
+ <img
+ class="icon light"
+ src="chrome://browser/content/logos/tracking-protection.svg"
+ />
+ <img
+ class="icon dark"
+ src="chrome://browser/content/logos/tracking-protection-dark-theme.svg"
+ />
+ <div class="wrapper">
+ <div>
+ <h2 class="card-title" data-l10n-id="etp-card-title-always"></h2>
+ <p
+ id="etp-card-content"
+ class="content"
+ data-l10n-id="etp-card-content-description"
+ ></p>
+ </div>
+ <a
+ target="_blank"
+ id="manage-protections"
+ data-l10n-id="protection-report-manage-protections"
+ ></a>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="body-wrapper">
+ <p id="graph-week-summary"></p>
+ <div id="graph-wrapper">
+ <div
+ id="graph"
+ role="table"
+ aria-labelledby="graphLegendDescription"
+ >
+ <div
+ id="private-window-message"
+ data-l10n-id="graph-private-window"
+ ></div>
+ </div>
+ <div id="legend">
+ <label
+ id="graphLegendDescription"
+ data-l10n-id="graph-legend-description"
+ ></label>
+ <input
+ id="tab-social"
+ data-type="social"
+ type="radio"
+ name="tabs"
+ aria-labelledby="socialLabel socialTitle"
+ aria-describedby="socialContent"
+ checked
+ />
+ <label id="socialLabel" for="tab-social" data-type="social">
+ <img
+ class="icon-small"
+ src="chrome://browser/skin/thumb-down.svg"
+ data-type="social"
+ />
+ <span data-type="social"></span>
+ </label>
+
+ <input
+ id="tab-cookie"
+ data-type="cookie"
+ type="radio"
+ name="tabs"
+ aria-labelledby="cookieLabel cookieTitle"
+ aria-describedby="cookieContent"
+ />
+ <label id="cookieLabel" for="tab-cookie" data-type="cookie">
+ <img
+ class="icon-small"
+ src="chrome://browser/skin/controlcenter/3rdpartycookies.svg"
+ data-type="cookie"
+ />
+ <span data-type="cookie"></span>
+ </label>
+
+ <input
+ id="tab-tracker"
+ data-type="tracker"
+ type="radio"
+ name="tabs"
+ aria-labelledby="trackerLabel trackerTitle"
+ aria-describedby="trackerContent"
+ />
+ <label id="trackerLabel" for="tab-tracker" data-type="tracker">
+ <img
+ class="icon-small"
+ src="chrome://browser/skin/canvas.svg"
+ data-type="tracker"
+ />
+ <span data-type="tracker"></span>
+ </label>
+
+ <input
+ id="tab-fingerprinter"
+ data-type="fingerprinter"
+ type="radio"
+ name="tabs"
+ aria-labelledby="fingerprinterLabel fingerprinterTitle"
+ aria-describedby="fingerprinterContent"
+ />
+ <label
+ id="fingerprinterLabel"
+ for="tab-fingerprinter"
+ data-type="fingerprinter"
+ >
+ <img
+ class="icon-small"
+ src="chrome://browser/skin/fingerprint.svg"
+ data-type="fingerprinter"
+ />
+ <span data-type="fingerprinter"></span>
+ </label>
+
+ <input
+ id="tab-cryptominer"
+ data-type="cryptominer"
+ type="radio"
+ name="tabs"
+ aria-labelledby="cryptominerLabel cryptominerTitle"
+ aria-describedby="cryptominerContent"
+ />
+ <label
+ id="cryptominerLabel"
+ for="tab-cryptominer"
+ data-type="cryptominer"
+ >
+ <img
+ class="icon-small"
+ src="chrome://browser/skin/controlcenter/cryptominers.svg"
+ data-type="cryptominer"
+ />
+ <span data-type="cryptominer"></span>
+ </label>
+ <div id="highlight"></div>
+ <div id="highlight-hover"></div>
+ <div id="social" class="tab-content">
+ <p
+ id="socialTitle"
+ class="content-title"
+ data-l10n-id="social-tab-title"
+ ></p>
+ <p id="socialContent" data-l10n-id="social-tab-contant">
+ <a
+ target="_blank"
+ id="social-link"
+ data-l10n-name="learn-more-link"
+ ></a>
+ </p>
+ </div>
+ <div id="cookie" class="tab-content">
+ <p
+ id="cookieTitle"
+ class="content-title"
+ data-l10n-id="cookie-tab-title"
+ ></p>
+ <p id="cookieContent" data-l10n-id="cookie-tab-content">
+ <a
+ target="_blank"
+ id="cookie-link"
+ data-l10n-name="learn-more-link"
+ ></a>
+ </p>
+ </div>
+ <div id="tracker" class="tab-content">
+ <p
+ id="trackerTitle"
+ class="content-title"
+ data-l10n-id="tracker-tab-title"
+ ></p>
+ <p id="trackerContent" data-l10n-id="tracker-tab-description">
+ <a
+ target="_blank"
+ id="tracker-link"
+ data-l10n-name="learn-more-link"
+ ></a>
+ </p>
+ </div>
+ <div id="fingerprinter" class="tab-content">
+ <p
+ id="fingerprinterTitle"
+ class="content-title"
+ data-l10n-id="fingerprinter-tab-title"
+ ></p>
+ <p
+ id="fingerprinterContent"
+ data-l10n-id="fingerprinter-tab-content"
+ >
+ <a
+ target="_blank"
+ id="fingerprinter-link"
+ data-l10n-name="learn-more-link"
+ ></a>
+ </p>
+ </div>
+ <div id="cryptominer" class="tab-content">
+ <p
+ id="cryptominerTitle"
+ class="content-title"
+ data-l10n-id="cryptominer-tab-title"
+ ></p>
+ <p
+ id="cryptominerContent"
+ data-l10n-id="cryptominer-tab-content"
+ >
+ <a
+ target="_blank"
+ id="cryptominer-link"
+ data-l10n-name="learn-more-link"
+ ></a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div id="graph-total-summary"></div>
+ </div>
+ </div>
+ <div id="mobile-hanger" class="card-body hidden">
+ <div class="body-wrapper">
+ <button
+ class="exit-icon"
+ data-l10n-id="protections-close-button2"
+ ></button>
+ <div id="etp-mobile-content">
+ <img
+ class="mobile-app-icon"
+ src="chrome://browser/content/logos/etp-mobile.svg"
+ />
+ <span>
+ <h2 class="card-title" data-l10n-id="mobile-app-title"></h2>
+ <p class="content">
+ <span data-l10n-id="mobile-app-card-content"></span>
+ <span
+ target="_blank"
+ id="mobile-app-links"
+ data-l10n-id="mobile-app-links"
+ >
+ <a
+ target="_blank"
+ id="android-mobile-inline-link"
+ data-l10n-name="android-mobile-inline-link"
+ href=""
+ ></a>
+ <a
+ target="_blank"
+ id="ios-mobile-inline-link"
+ data-l10n-name="ios-mobile-inline-link"
+ href=""
+ ></a>
+ </span>
+ </p>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- Markup for Monitor card. -->
+ <section class="card card-no-hover monitor-card hidden">
+ <div class="card-header">
+ <img class="icon" src="chrome://browser/content/logos/monitor.svg" />
+ <div class="wrapper">
+ <div>
+ <h2
+ id="monitor-title"
+ class="card-title"
+ data-l10n-id="monitor-title"
+ ></h2>
+ <p id="monitor-header-content" class="content">
+ <span>
+ <!-- Insert Monitor header content here. -->
+ </span>
+ <a
+ target="_blank"
+ href=""
+ id="monitor-link"
+ data-l10n-id="monitor-link"
+ ></a>
+ </p>
+ <div class="monitor-scanned-wrapper">
+ <img
+ class="icon-small"
+ src="chrome://global/skin/icons/reload.svg"
+ />
+ <span
+ class="monitor-scanned-text"
+ data-l10n-id="auto-scan"
+ ></span>
+ </div>
+ </div>
+ <a target="_blank" id="sign-up-for-monitor-link">
+ <!-- Insert Monitor link content here. -->
+ </a>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="body-wrapper">
+ <div id="monitor-body-content">
+ <div class="monitor-info-wrapper">
+ <div class="monitor-block email">
+ <a
+ target="_blank"
+ id="monitor-stored-emails-link"
+ data-l10n-id="monitor-emails-tooltip"
+ >
+ <span class="monitor-stat">
+ <img
+ class="icon-med"
+ src="chrome://browser/skin/mail.svg"
+ />
+ <span data-type="stored-emails">
+ <!-- Display number of stored emails here. -->
+ </span>
+ </span>
+ <span
+ id="info-monitored-addresses"
+ class="info-text"
+ ></span>
+ </a>
+ </div>
+ <div class="monitor-block breaches">
+ <a
+ target="_blank"
+ id="monitor-known-breaches-link"
+ data-l10n-id="monitor-breaches-tooltip"
+ >
+ <span class="monitor-stat">
+ <img
+ class="icon-med"
+ src="chrome://browser/skin/fxa/avatar.svg"
+ />
+ <span data-type="known-breaches">
+ <!-- Display number of known breaches here. -->
+ </span>
+ </span>
+ <span id="info-known-breaches" class="info-text"></span>
+ </a>
+ </div>
+ <div class="monitor-block passwords">
+ <a
+ target="_blank"
+ id="monitor-exposed-passwords-link"
+ data-l10n-id="monitor-passwords-tooltip"
+ >
+ <span class="monitor-stat">
+ <img
+ class="icon-med"
+ src="chrome://browser/skin/login.svg"
+ />
+ <span data-type="exposed-passwords">
+ <!-- Display number of exposed passwords here. -->
+ </span>
+ </span>
+ <span id="info-exposed-passwords" class="info-text"></span>
+ </a>
+ </div>
+ </div>
+ <div class="monitor-breaches-wrapper">
+ <div class="monitor-breaches-header">
+ <img id="monitor-breaches-icon" />
+ <span id="monitor-breaches-title" />
+ </div>
+ <div class="monitor-breaches-description-wrapper">
+ <span
+ id="monitor-breaches-description"
+ class="content"
+ ></span>
+ <div class="monitor-breaches-link-wrapper">
+ <a id="monitor-breaches-link" target="_blank"></a>
+ </div>
+ </div>
+ </div>
+ <div class="monitor-partial-breaches-wrapper">
+ <div class="monitor-partial-breaches-header">
+ <img
+ id="monitor-partial-breaches-icon"
+ src="chrome://browser/skin/protections/resolved-breach-gray.svg"
+ />
+ <span id="monitor-partial-breaches-title"></span>
+ <span id="monitor-partial-breaches-percentage"></span>
+ </div>
+ <progress class="progress-bar" max="100"></progress>
+ <div class="monitor-partial-breaches-motivation-text">
+ <span id="monitor-partial-breaches-motivation-title"></span>
+ <div class="monitor-partial-breaches-motivation-wrapper">
+ <span
+ id="monitor-partial-breaches-motivation-desc"
+ class="content"
+ data-l10n-id="monitor-partial-breaches-motivation-description"
+ ></span>
+ <div class="monitor-partial-breaches-link-wrapper">
+ <a
+ id="monitor-partial-breaches-link"
+ target="_blank"
+ data-l10n-id="monitor-resolve-breaches-link"
+ ></a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <!-- Markup for passwords card. -->
+ <section class="card card-no-hover lockwise-card hidden">
+ <div class="card-header">
+ <img class="icon" src="chrome://browser/content/logos/lockwise.svg" />
+ <div class="wrapper">
+ <div>
+ <h2 id="lockwise-title" class="card-title">
+ <!-- Insert Lockwise card title here. -->
+ </h2>
+ <p id="lockwise-header-content" class="content">
+ <span>
+ <!-- Insert Lockwise header content here. -->
+ </span>
+ <a
+ target="_blank"
+ id="lockwise-how-it-works"
+ data-l10n-id="lockwise-how-it-works-link"
+ href=""
+ ></a>
+ </p>
+ </div>
+ <button
+ id="save-passwords-button"
+ class="primary"
+ data-l10n-id="protection-report-passwords-save-passwords-button"
+ ></button>
+ <div class="lockwise-scanned-wrapper">
+ <img id="lockwise-scanned-icon" />
+ <span id="lockwise-scanned-text" class="content">
+ <!-- Display message for stored logins here. -->
+ </span>
+ </div>
+ <button
+ id="manage-passwords-button"
+ class="primary"
+ data-l10n-id="protection-report-passwords-manage-passwords-button"
+ ></button>
+ </div>
+ </div>
+ <!-- <div class="card-body hidden">
+ <div id="lockwise-body-content" class="body-wrapper">
+ <div class="no-logins hidden">
+ </div>
+ </div>
+ </div> -->
+ </section>
+ <!-- Markup for Proxy card -->
+ <section class="card card-no-hover proxy-card hidden">
+ <div class="card-header">
+ <img
+ class="icon light"
+ src="chrome://browser/content/logos/proxy-light.svg"
+ />
+ <img
+ class="icon dark"
+ src="chrome://browser/content/logos/proxy-dark.svg"
+ />
+ <div class="wrapper">
+ <div>
+ <h3 class="card-title" data-l10n-id="proxy-title"></h3>
+ <p class="content" data-l10n-id="proxy-header-content"></p>
+ </div>
+ <a
+ target="_blank"
+ id="get-proxy-extension-link"
+ data-l10n-id="get-proxy-extension-link"
+ ></a>
+ </div>
+ </div>
+ </section>
+
+ <section class="card card-no-hover vpn-card hidden">
+ <div class="card-header">
+ <img
+ class="icon light"
+ src="chrome://browser/content/logos/vpn-light.svg"
+ />
+ <img
+ class="icon dark"
+ src="chrome://browser/content/logos/vpn-dark.svg"
+ />
+ <div class="wrapper">
+ <div>
+ <div class="title-wrapper">
+ <img
+ id="check-icon"
+ src="chrome://browser/skin/protections/resolved-breach.svg"
+ />
+ <h3 class="card-title" data-l10n-id="vpn-title"></h3>
+ </div>
+ <p class="content" data-l10n-id="vpn-header-content"></p>
+ <p
+ class="content subscribed"
+ data-l10n-id="vpn-header-content-subscribed"
+ >
+ <a
+ target="_blank"
+ id="vpn-google-playstore-link"
+ data-l10n-name="vpn-google-playstore-link"
+ ></a>
+ <a
+ target="_blank"
+ id="vpn-app-store-link"
+ data-l10n-name="vpn-app-store-link"
+ ></a>
+ </p>
+ </div>
+ <a
+ target="_blank"
+ id="get-vpn-link"
+ data-l10n-id="get-vpn-link"
+ ></a>
+ </div>
+ </div>
+ </section>
+ </div>
+ </body>
+</html>
diff --git a/browser/components/protections/content/protections.mjs b/browser/components/protections/content/protections.mjs
new file mode 100644
index 0000000000..3204586a2b
--- /dev/null
+++ b/browser/components/protections/content/protections.mjs
@@ -0,0 +1,490 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+import LockwiseCard from "./lockwise-card.mjs";
+import MonitorCard from "./monitor-card.mjs";
+import ProxyCard from "./proxy-card.mjs";
+import VPNCard from "./vpn-card.mjs";
+
+let cbCategory = RPMGetStringPref("browser.contentblocking.category");
+document.sendTelemetryEvent = (action, object, value = "") => {
+ // eslint-disable-next-line no-undef
+ RPMRecordTelemetryEvent("security.ui.protections", action, object, value, {
+ category: cbCategory,
+ });
+};
+
+let { protocol, pathname, searchParams } = new URL(document.location);
+
+let searchParamsChanged = false;
+if (searchParams.has("entrypoint")) {
+ RPMSendAsyncMessage("RecordEntryPoint", {
+ entrypoint: searchParams.get("entrypoint"),
+ });
+ // Remove this parameter from the URL (after recording above) to make it
+ // cleaner for bookmarking and switch-to-tab and so that bookmarked values
+ // don't skew telemetry.
+ searchParams.delete("entrypoint");
+ searchParamsChanged = true;
+}
+
+document.addEventListener("DOMContentLoaded", e => {
+ if (searchParamsChanged) {
+ let newURL = protocol + pathname;
+ let params = searchParams.toString();
+ if (params) {
+ newURL += "?" + params;
+ }
+ window.location.replace(newURL);
+ return;
+ }
+
+ RPMSendQuery("FetchEntryPoint", {}).then(entrypoint => {
+ // Send telemetry on arriving on this page
+ document.sendTelemetryEvent("show", "protection_report", entrypoint);
+ });
+
+ // We need to send the close telemetry before unload while we still have a connection to RPM.
+ window.addEventListener("beforeunload", () => {
+ document.sendTelemetryEvent("close", "protection_report");
+ });
+
+ let todayInMs = Date.now();
+ let weekAgoInMs = todayInMs - 6 * 24 * 60 * 60 * 1000;
+
+ let dataTypes = [
+ "cryptominer",
+ "fingerprinter",
+ "tracker",
+ "cookie",
+ "social",
+ ];
+
+ let manageProtectionsLink = document.getElementById("protection-settings");
+ let manageProtections = document.getElementById("manage-protections");
+ let protectionSettingsEvtHandler = evt => {
+ if (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click") {
+ RPMSendAsyncMessage("OpenContentBlockingPreferences");
+ if (evt.target.id == "protection-settings") {
+ document.sendTelemetryEvent(
+ "click",
+ "settings_link",
+ "header-settings"
+ );
+ } else if (evt.target.id == "manage-protections") {
+ document.sendTelemetryEvent(
+ "click",
+ "settings_link",
+ "custom-card-settings"
+ );
+ }
+ }
+ };
+ manageProtectionsLink.addEventListener("click", protectionSettingsEvtHandler);
+ manageProtectionsLink.addEventListener(
+ "keypress",
+ protectionSettingsEvtHandler
+ );
+ manageProtections.addEventListener("click", protectionSettingsEvtHandler);
+ manageProtections.addEventListener("keypress", protectionSettingsEvtHandler);
+
+ let legend = document.getElementById("legend");
+ legend.style.gridTemplateAreas =
+ "'social cookie tracker fingerprinter cryptominer'";
+
+ let createGraph = data => {
+ let graph = document.getElementById("graph");
+ let summary = document.getElementById("graph-total-summary");
+ let weekSummary = document.getElementById("graph-week-summary");
+
+ // User is in private mode, show no data on the graph
+ if (data.isPrivate) {
+ graph.classList.add("private-window");
+ } else {
+ let earliestDate = data.earliestDate || Date.now();
+ document.l10n.setAttributes(summary, "graph-total-tracker-summary", {
+ count: data.sumEvents,
+ earliestDate,
+ });
+ }
+
+ // Set a default top size for the height of the graph bars so that small
+ // numbers don't fill the whole graph.
+ let largest = 100;
+ if (largest < data.largest) {
+ largest = data.largest;
+ }
+ let weekCount = 0;
+ let weekTypeCounts = {
+ social: 0,
+ cookie: 0,
+ tracker: 0,
+ fingerprinter: 0,
+ cryptominer: 0,
+ };
+
+ // For accessibility clients, we turn the graph into a fake table with annotated text.
+ // We use WAI-ARIA roles, properties, and states to mark up the table, rows and cells.
+ // Each day becomes one row in the table.
+ // Each row contains the day, total, and then one cell for each bar that we display.
+ // At most, a row can contain seven cells.
+ // But we need to caclulate the actual number of the most cells in a row to give accurate information.
+ let maxColumnCount = 0;
+ let date = new Date();
+ for (let i = 0; i <= 6; i++) {
+ let dateString = date.toISOString().split("T")[0];
+ let ariaOwnsString = ""; // Get the row's colummns in order
+ let currentColumnCount = 0;
+ let bar = document.createElement("div");
+ bar.className = "graph-bar";
+ bar.setAttribute("role", "row");
+ let innerBar = document.createElement("div");
+ innerBar.className = "graph-wrapper-bar";
+ if (data[dateString]) {
+ let content = data[dateString];
+ let count = document.createElement("div");
+ count.className = "bar-count";
+ count.id = "count" + i;
+ count.setAttribute("role", "cell");
+ count.textContent = content.total;
+ setTimeout(() => {
+ count.classList.add("animate");
+ }, 400);
+ bar.appendChild(count);
+ ariaOwnsString = count.id;
+ currentColumnCount += 1;
+ let barHeight = (content.total / largest) * 100;
+ weekCount += content.total;
+ // Add a short timeout to allow the elements to be added to the dom before triggering an animation.
+ setTimeout(() => {
+ bar.style.height = `${barHeight}%`;
+ }, 20);
+ for (let type of dataTypes) {
+ if (content[type]) {
+ let dataHeight = (content[type] / content.total) * 100;
+ // Since we are dealing with non-visual content, screen readers need a parent container to get the text
+ let cellSpan = document.createElement("span");
+ cellSpan.id = type + i;
+ cellSpan.setAttribute("role", "cell");
+ let div = document.createElement("div");
+ div.className = `${type}-bar inner-bar`;
+ div.setAttribute("role", "img");
+ div.setAttribute("data-type", type);
+ div.style.height = `${dataHeight}%`;
+ document.l10n.setAttributes(div, `bar-tooltip-${type}`, {
+ count: content[type],
+ percentage: dataHeight,
+ });
+ weekTypeCounts[type] += content[type];
+ cellSpan.appendChild(div);
+ innerBar.appendChild(cellSpan);
+ ariaOwnsString = ariaOwnsString + " " + cellSpan.id;
+ currentColumnCount += 1;
+ }
+ }
+ if (currentColumnCount > maxColumnCount) {
+ // The current row has more than any previous rows
+ maxColumnCount = currentColumnCount;
+ }
+ } else {
+ // There were no content blocking events on this day.
+ bar.classList.add("empty");
+ }
+ bar.appendChild(innerBar);
+ graph.prepend(bar);
+
+ if (data.isPrivate) {
+ document.l10n.setAttributes(
+ weekSummary,
+ "graph-week-summary-private-window"
+ );
+ } else {
+ document.l10n.setAttributes(weekSummary, "graph-week-summary", {
+ count: weekCount,
+ });
+ }
+
+ let label = document.createElement("span");
+ label.className = "column-label";
+ // While the graphs fill up from the right, the days fill up from the left, so match the IDs
+ label.id = "day" + (6 - i);
+ label.setAttribute("role", "rowheader");
+ if (i == 6) {
+ document.l10n.setAttributes(label, "graph-today");
+ } else {
+ label.textContent = data.weekdays[(i + 1 + new Date().getDay()) % 7];
+ }
+ graph.append(label);
+ // Make the day the first column in a row, making it the row header.
+ bar.setAttribute("aria-owns", "day" + i + " " + ariaOwnsString);
+ date.setDate(date.getDate() - 1);
+ }
+ maxColumnCount += 1; // Add the day column in the fake table
+ graph.setAttribute("aria-colCount", maxColumnCount);
+ // Set the total number of each type of tracker on the tabs as well as their
+ // "Learn More" links
+ for (let type of dataTypes) {
+ document.querySelector(`label[data-type=${type}] span`).textContent =
+ weekTypeCounts[type];
+ const learnMoreLink = document.getElementById(`${type}-link`);
+ learnMoreLink.href = RPMGetFormatURLPref(
+ `browser.contentblocking.report.${type}.url`
+ );
+ learnMoreLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "trackers_about_link", type);
+ });
+ }
+
+ let blockingCookies =
+ RPMGetIntPref("network.cookie.cookieBehavior", 0) != 0;
+ let cryptominingEnabled = RPMGetBoolPref(
+ "privacy.trackingprotection.cryptomining.enabled",
+ false
+ );
+ let fingerprintingEnabled =
+ RPMGetBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ false
+ ) || RPMGetBoolPref("privacy.fingerprintingProtection", false);
+ let tpEnabled = RPMGetBoolPref("privacy.trackingprotection.enabled", false);
+ let socialTracking = RPMGetBoolPref(
+ "privacy.trackingprotection.socialtracking.enabled",
+ false
+ );
+ let socialCookies = RPMGetBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ false
+ );
+ let socialEnabled =
+ socialCookies && (blockingCookies || (tpEnabled && socialTracking));
+ let notBlocking =
+ !blockingCookies &&
+ !cryptominingEnabled &&
+ !fingerprintingEnabled &&
+ !tpEnabled &&
+ !socialEnabled;
+
+ // User has turned off all blocking, show a different card.
+ if (notBlocking) {
+ document.l10n.setAttributes(
+ document.getElementById("etp-card-content"),
+ "protection-report-etp-card-content-custom-not-blocking"
+ );
+ document.l10n.setAttributes(
+ document.querySelector(".etp-card .card-title"),
+ "etp-card-title-custom-not-blocking"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("report-summary"),
+ "protection-report-page-summary"
+ );
+ document.querySelector(".etp-card").classList.add("custom-not-blocking");
+
+ // Hide the link to settings from the header, so we are not showing two links.
+ manageProtectionsLink.style.display = "none";
+ } else {
+ // Hide each type of tab if blocking of that type is off.
+ if (!tpEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "tracker",
+ ""
+ );
+ let radio = document.getElementById("tab-tracker");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-tracker ~ label").style.display = "none";
+ }
+ if (!socialEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "social",
+ ""
+ );
+ let radio = document.getElementById("tab-social");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-social ~ label").style.display = "none";
+ }
+ if (!blockingCookies) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "cookie",
+ ""
+ );
+ let radio = document.getElementById("tab-cookie");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-cookie ~ label").style.display = "none";
+ }
+ if (!cryptominingEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "cryptominer",
+ ""
+ );
+ let radio = document.getElementById("tab-cryptominer");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-cryptominer ~ label").style.display =
+ "none";
+ }
+ if (!fingerprintingEnabled) {
+ legend.style.gridTemplateAreas = legend.style.gridTemplateAreas.replace(
+ "fingerprinter",
+ ""
+ );
+ let radio = document.getElementById("tab-fingerprinter");
+ radio.setAttribute("disabled", true);
+ document.querySelector("#tab-fingerprinter ~ label").style.display =
+ "none";
+ }
+
+ let firstRadio = document.querySelector("input:enabled");
+ // There will be no radio options if we are showing the
+ firstRadio.checked = true;
+ document.body.setAttribute("focuseddatatype", firstRadio.dataset.type);
+
+ addListeners();
+ }
+ };
+
+ let addListeners = () => {
+ let wrapper = document.querySelector(".body-wrapper");
+ let triggerTabClick = ev => {
+ if (ev.originalTarget.dataset.type) {
+ document.getElementById(`tab-${ev.target.dataset.type}`).click();
+ }
+ };
+
+ let triggerTabFocus = ev => {
+ if (ev.originalTarget.dataset) {
+ wrapper.classList.add("hover-" + ev.originalTarget.dataset.type);
+ }
+ };
+
+ let triggerTabBlur = ev => {
+ if (ev.originalTarget.dataset) {
+ wrapper.classList.remove("hover-" + ev.originalTarget.dataset.type);
+ }
+ };
+ wrapper.addEventListener("mouseout", triggerTabBlur);
+ wrapper.addEventListener("mouseover", triggerTabFocus);
+ wrapper.addEventListener("click", triggerTabClick);
+
+ // Change the class on the body to change the color variable.
+ let radios = document.querySelectorAll("#legend input");
+ for (let radio of radios) {
+ radio.addEventListener("change", ev => {
+ document.body.setAttribute("focuseddatatype", ev.target.dataset.type);
+ });
+ radio.addEventListener("focus", ev => {
+ wrapper.classList.add("hover-" + ev.originalTarget.dataset.type);
+ document.body.setAttribute("focuseddatatype", ev.target.dataset.type);
+ });
+ radio.addEventListener("blur", ev => {
+ wrapper.classList.remove("hover-" + ev.originalTarget.dataset.type);
+ });
+ }
+ };
+
+ RPMSendQuery("FetchContentBlockingEvents", {
+ from: weekAgoInMs,
+ to: todayInMs,
+ }).then(createGraph);
+
+ let exitIcon = document.querySelector("#mobile-hanger .exit-icon");
+ // hide the mobile promotion and keep hidden with a pref.
+ exitIcon.addEventListener("click", () => {
+ RPMSetPref("browser.contentblocking.report.show_mobile_app", false);
+ document.getElementById("mobile-hanger").classList.add("hidden");
+ });
+
+ let androidMobileAppLink = document.getElementById(
+ "android-mobile-inline-link"
+ );
+ androidMobileAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.mobile-android.url"
+ );
+ androidMobileAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "mobile_app_link", "android");
+ });
+ let iosMobileAppLink = document.getElementById("ios-mobile-inline-link");
+ iosMobileAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.mobile-ios.url"
+ );
+ iosMobileAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "mobile_app_link", "ios");
+ });
+
+ let lockwiseEnabled = RPMGetBoolPref(
+ "browser.contentblocking.report.lockwise.enabled",
+ true
+ );
+
+ let lockwiseCard;
+ if (lockwiseEnabled) {
+ const lockwiseUI = document.querySelector(".lockwise-card");
+ lockwiseUI.classList.remove("hidden");
+ lockwiseUI.classList.add("loading");
+
+ lockwiseCard = new LockwiseCard(document);
+ lockwiseCard.init();
+ }
+
+ RPMSendQuery("FetchUserLoginsData", {}).then(data => {
+ if (lockwiseCard) {
+ // Once data for the user is retrieved, display the lockwise card.
+ lockwiseCard.buildContent(data);
+ }
+
+ if (
+ RPMGetBoolPref("browser.contentblocking.report.show_mobile_app") &&
+ !data.mobileDeviceConnected
+ ) {
+ document
+ .getElementById("mobile-hanger")
+ .classList.toggle("hidden", false);
+ }
+ });
+
+ // For tests
+ const lockwiseUI = document.querySelector(".lockwise-card");
+ lockwiseUI.dataset.enabled = lockwiseEnabled;
+
+ let monitorEnabled = RPMGetBoolPref(
+ "browser.contentblocking.report.monitor.enabled",
+ true
+ );
+ if (monitorEnabled) {
+ // Show the Monitor card.
+ const monitorUI = document.querySelector(".card.monitor-card.hidden");
+ monitorUI.classList.remove("hidden");
+ monitorUI.classList.add("loading");
+
+ const monitorCard = new MonitorCard(document);
+ monitorCard.init();
+ }
+
+ // For tests
+ const monitorUI = document.querySelector(".monitor-card");
+ monitorUI.dataset.enabled = monitorEnabled;
+
+ const proxyEnabled = RPMGetBoolPref(
+ "browser.contentblocking.report.proxy.enabled",
+ true
+ );
+
+ if (proxyEnabled) {
+ const proxyCard = new ProxyCard(document);
+ proxyCard.init();
+ }
+
+ // For tests
+ const proxyUI = document.querySelector(".proxy-card");
+ proxyUI.dataset.enabled = proxyEnabled;
+
+ const VPNEnabled = RPMGetBoolPref("browser.vpn_promo.enabled", true);
+ if (VPNEnabled) {
+ const vpnCard = new VPNCard(document);
+ vpnCard.init();
+ }
+ // For tests
+ const vpnUI = document.querySelector(".vpn-card");
+ vpnUI.dataset.enabled = VPNEnabled;
+});
diff --git a/browser/components/protections/content/proxy-card.mjs b/browser/components/protections/content/proxy-card.mjs
new file mode 100644
index 0000000000..bc6df810b4
--- /dev/null
+++ b/browser/components/protections/content/proxy-card.mjs
@@ -0,0 +1,29 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+const PROXY_EXTENSION_URL = RPMGetStringPref(
+ "browser.contentblocking.report.proxy_extension.url",
+ ""
+);
+
+export default class ProxyCard {
+ constructor(doc) {
+ this.doc = doc;
+ }
+
+ init() {
+ const proxyExtensionLink = this.doc.getElementById(
+ "get-proxy-extension-link"
+ );
+ proxyExtensionLink.href = PROXY_EXTENSION_URL;
+
+ // Show the Proxy card
+ RPMSendQuery("GetShowProxyCard", {}).then(shouldShow => {
+ const proxyCard = this.doc.querySelector(".proxy-card");
+ proxyCard.classList.toggle("hidden", !shouldShow);
+ });
+ }
+}
diff --git a/browser/components/protections/content/vpn-card.mjs b/browser/components/protections/content/vpn-card.mjs
new file mode 100644
index 0000000000..dd2039db2e
--- /dev/null
+++ b/browser/components/protections/content/vpn-card.mjs
@@ -0,0 +1,103 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+export default class VPNCard {
+ constructor(doc) {
+ this.doc = doc;
+ }
+
+ init() {
+ const vpnLink = this.doc.getElementById("get-vpn-link");
+ const vpnBannerLink = this.doc.getElementById("vpn-banner-link");
+ vpnLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn.url",
+ ""
+ );
+ vpnBannerLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn-promo.url",
+ ""
+ );
+
+ vpnLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "vpn_card_link");
+ });
+ let androidVPNAppLink = document.getElementById(
+ "vpn-google-playstore-link"
+ );
+ androidVPNAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn-android.url"
+ );
+ androidVPNAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "vpn_app_link_android");
+ });
+ let iosVPNAppLink = document.getElementById("vpn-app-store-link");
+ iosVPNAppLink.href = RPMGetStringPref(
+ "browser.contentblocking.report.vpn-ios.url"
+ );
+ iosVPNAppLink.addEventListener("click", () => {
+ document.sendTelemetryEvent("click", "vpn_app_link_ios");
+ });
+
+ const vpnBanner = this.doc.querySelector(".vpn-banner");
+ const exitIcon = vpnBanner.querySelector(".exit-icon");
+ vpnBannerLink.addEventListener("click", () => {
+ this.doc.sendTelemetryEvent("click", "vpn_banner_link");
+ });
+ // User has closed the vpn banner, hide it.
+ exitIcon.addEventListener("click", () => {
+ vpnBanner.classList.add("hidden");
+ this.doc.sendTelemetryEvent("click", "vpn_banner_close");
+ });
+
+ this.showVPNCard();
+ }
+
+ // Show the VPN card if user is located in areas, and on platforms, it serves
+ async showVPNCard() {
+ const showVPNBanner = this.showVPNBanner.bind(this);
+ RPMSendQuery("FetchShowVPNCard", {}).then(shouldShow => {
+ if (!shouldShow) {
+ return;
+ }
+ const vpnCard = this.doc.querySelector(".vpn-card");
+
+ // add 'subscribed' class if user is subscribed to vpn
+ RPMSendQuery("FetchVPNSubStatus", {}).then(async hasVPN => {
+ if (hasVPN) {
+ vpnCard.classList.add("subscribed");
+ document.l10n.setAttributes(
+ vpnCard.querySelector(".card-title"),
+ "vpn-title-subscribed"
+ );
+
+ // hide the promo banner if the user is already subscribed to vpn
+ await RPMSetPref(
+ "browser.contentblocking.report.hide_vpn_banner",
+ true
+ );
+ }
+
+ vpnCard.classList.remove("hidden");
+ showVPNBanner();
+ });
+ });
+ }
+
+ showVPNBanner() {
+ if (
+ RPMGetBoolPref("browser.contentblocking.report.hide_vpn_banner", false) ||
+ !RPMGetBoolPref("browser.vpn_promo.enabled", false)
+ ) {
+ return;
+ }
+
+ const vpnBanner = this.doc.querySelector(".vpn-banner");
+ vpnBanner.classList.remove("hidden");
+ this.doc.sendTelemetryEvent("show", "vpn_banner");
+ // VPN banner only shows on the first visit, flip a pref so it does not show again.
+ RPMSetPref("browser.contentblocking.report.hide_vpn_banner", true);
+ }
+}
diff --git a/browser/components/protections/jar.mn b/browser/components/protections/jar.mn
new file mode 100644
index 0000000000..db8117d2f6
--- /dev/null
+++ b/browser/components/protections/jar.mn
@@ -0,0 +1,12 @@
+# 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/.
+
+browser.jar:
+ content/browser/lockwise-card.mjs (content/lockwise-card.mjs)
+ content/browser/monitor-card.mjs (content/monitor-card.mjs)
+ content/browser/protections.css (content/protections.css)
+ content/browser/protections.html (content/protections.html)
+ content/browser/protections.mjs (content/protections.mjs)
+ content/browser/proxy-card.mjs (content/proxy-card.mjs)
+ content/browser/vpn-card.mjs (content/vpn-card.mjs)
diff --git a/browser/components/protections/moz.build b/browser/components/protections/moz.build
new file mode 100644
index 0000000000..c79fefcc16
--- /dev/null
+++ b/browser/components/protections/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Protections UI")
diff --git a/browser/components/protections/test/browser/browser.toml b/browser/components/protections/test/browser/browser.toml
new file mode 100644
index 0000000000..2cea61d997
--- /dev/null
+++ b/browser/components/protections/test/browser/browser.toml
@@ -0,0 +1,18 @@
+[DEFAULT]
+prefs = ["toolkit.telemetry.ipcBatchTimeout=0"]
+support-files = [
+ "head.js",
+ "!/browser/base/content/test/protectionsUI/trackingPage.html",
+]
+
+["browser_protections_lockwise.js"]
+
+["browser_protections_monitor.js"]
+
+["browser_protections_proxy.js"]
+
+["browser_protections_report_ui.js"]
+
+["browser_protections_telemetry.js"]
+
+["browser_protections_vpn.js"]
diff --git a/browser/components/protections/test/browser/browser_protections_lockwise.js b/browser/components/protections/test/browser/browser_protections_lockwise.js
new file mode 100644
index 0000000000..72e1795455
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_lockwise.js
@@ -0,0 +1,290 @@
+/* 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";
+
+requestLongerTimeout(2);
+
+const { AboutProtectionsParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutProtectionsParent.sys.mjs"
+);
+const ABOUT_LOGINS_URL = "about:logins";
+
+add_task(async function testNoLoginsLockwiseCardUI() {
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ const aboutLoginsPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ ABOUT_LOGINS_URL
+ );
+
+ info(
+ "Check that the correct lockwise card content is displayed for non-logged in users."
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ const lockwiseCard = content.document.querySelector(".lockwise-card");
+ return ContentTaskUtils.isVisible(lockwiseCard);
+ }, "Lockwise card for user with no logins is visible.");
+
+ const lockwiseHowItWorks = content.document.querySelector(
+ "#lockwise-how-it-works"
+ );
+ ok(
+ ContentTaskUtils.isHidden(lockwiseHowItWorks),
+ "How it works link is hidden"
+ );
+
+ const lockwiseHeaderContent = content.document.querySelector(
+ "#lockwise-header-content span"
+ );
+ await content.document.l10n.translateElements([lockwiseHeaderContent]);
+ is(
+ lockwiseHeaderContent.dataset.l10nId,
+ "passwords-header-content",
+ "lockwiseHeaderContent contents should match l10n-id attribute set on the element"
+ );
+
+ const lockwiseScannedWrapper = content.document.querySelector(
+ ".lockwise-scanned-wrapper"
+ );
+ ok(
+ ContentTaskUtils.isHidden(lockwiseScannedWrapper),
+ "Lockwise scanned wrapper is hidden"
+ );
+
+ const managePasswordsButton = content.document.querySelector(
+ "#manage-passwords-button"
+ );
+ ok(
+ ContentTaskUtils.isHidden(managePasswordsButton),
+ "Manage passwords button is hidden"
+ );
+
+ const savePasswordsButton = content.document.querySelector(
+ "#save-passwords-button"
+ );
+ ok(
+ ContentTaskUtils.isVisible(savePasswordsButton),
+ "Save passwords button is visible in the header"
+ );
+ info(
+ "Click on the save passwords button and check that it opens about:logins in a new tab"
+ );
+ savePasswordsButton.click();
+ });
+ const loginsTab = await aboutLoginsPromise;
+ info("about:logins was successfully opened in a new tab");
+ gBrowser.removeTab(loginsTab);
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testLockwiseCardUIWithLogins() {
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ const aboutLoginsPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ ABOUT_LOGINS_URL
+ );
+
+ info(
+ "Add a login and check that lockwise card content for a logged in user is displayed correctly"
+ );
+ await Services.logins.addLoginAsync(TEST_LOGIN1);
+ await BrowserTestUtils.reloadTab(tab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ const hasLogins = content.document.querySelector(".lockwise-card");
+ return ContentTaskUtils.isVisible(hasLogins);
+ }, "Lockwise card for user with logins is visible");
+
+ const lockwiseTitle = content.document.querySelector("#lockwise-title");
+ await content.document.l10n.translateElements([lockwiseTitle]);
+ await ContentTaskUtils.waitForCondition(
+ () => lockwiseTitle.textContent == "Manage your passwords",
+ "Waiting for Fluent to provide the title translation"
+ );
+ is(
+ lockwiseTitle.textContent,
+ "Manage your passwords",
+ "Correct passwords title is shown"
+ );
+
+ const lockwiseHowItWorks = content.document.querySelector(
+ "#lockwise-how-it-works"
+ );
+ ok(
+ ContentTaskUtils.isVisible(lockwiseHowItWorks),
+ "How it works link is visible"
+ );
+
+ const lockwiseHeaderContent = content.document.querySelector(
+ "#lockwise-header-content span"
+ );
+ await content.document.l10n.translateElements([lockwiseHeaderContent]);
+ is(
+ lockwiseHeaderContent.dataset.l10nId,
+ "lockwise-header-content-logged-in",
+ "lockwiseHeaderContent contents should match l10n-id attribute set on the element"
+ );
+
+ const lockwiseScannedWrapper = content.document.querySelector(
+ ".lockwise-scanned-wrapper"
+ );
+ ok(
+ ContentTaskUtils.isVisible(lockwiseScannedWrapper),
+ "Lockwise scanned wrapper is visible"
+ );
+
+ const lockwiseScannedText = content.document.querySelector(
+ "#lockwise-scanned-text"
+ );
+ await content.document.l10n.translateElements([lockwiseScannedText]);
+ is(
+ lockwiseScannedText.textContent,
+ "1 password stored securely.",
+ "Correct lockwise scanned text is shown"
+ );
+
+ const savePasswordsButton = content.document.querySelector(
+ "#save-passwords-button"
+ );
+ ok(
+ ContentTaskUtils.isHidden(savePasswordsButton),
+ "Save passwords button is hidden"
+ );
+
+ const managePasswordsButton = content.document.querySelector(
+ "#manage-passwords-button"
+ );
+ ok(
+ ContentTaskUtils.isVisible(managePasswordsButton),
+ "Manage passwords button is visible"
+ );
+ info(
+ "Click on the manage passwords button and check that it opens about:logins in a new tab"
+ );
+ managePasswordsButton.click();
+ });
+ const loginsTab = await aboutLoginsPromise;
+ info("about:logins was successfully opened in a new tab");
+ gBrowser.removeTab(loginsTab);
+
+ info(
+ "Add another login and check that the scanned text about stored logins is updated after reload."
+ );
+ await Services.logins.addLoginAsync(TEST_LOGIN2);
+ await BrowserTestUtils.reloadTab(tab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const lockwiseScannedText = content.document.querySelector(
+ "#lockwise-scanned-text"
+ ).textContent;
+ ContentTaskUtils.waitForCondition(
+ () =>
+ lockwiseScannedText.textContent ==
+ "Your passwords are being stored securely.",
+ "Correct lockwise scanned text is shown"
+ );
+ });
+
+ Services.logins.removeLogin(TEST_LOGIN1);
+ Services.logins.removeLogin(TEST_LOGIN2);
+
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testLockwiseCardUIWithBreachedLogins() {
+ info(
+ "Add a breached login and test that the lockwise scanned text is displayed correctly"
+ );
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await Services.logins.addLoginAsync(TEST_LOGIN1);
+
+ info("Mock monitor data with a breached login to test the Lockwise UI");
+ AboutProtectionsParent.setTestOverride(
+ mockGetLoginDataWithSyncedDevices(false, 1)
+ );
+ await BrowserTestUtils.reloadTab(tab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const lockwiseScannedText = content.document.querySelector(
+ "#lockwise-scanned-text"
+ );
+ ok(
+ ContentTaskUtils.isVisible(lockwiseScannedText),
+ "Lockwise scanned text is visible"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ lockwiseScannedText.textContent ==
+ "1 password may have been exposed in a data breach."
+ );
+ info("Correct lockwise scanned text is shown");
+ });
+
+ info(
+ "Mock monitor data with more than one breached logins to test the Lockwise UI"
+ );
+ AboutProtectionsParent.setTestOverride(
+ mockGetLoginDataWithSyncedDevices(false, 2)
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const lockwiseScannedText = content.document.querySelector(
+ "#lockwise-scanned-text"
+ );
+ ok(
+ ContentTaskUtils.isVisible(lockwiseScannedText),
+ "Lockwise scanned text is visible"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ lockwiseScannedText.textContent ==
+ "2 passwords may have been exposed in a data breach."
+ );
+ info("Correct lockwise scanned text is shown");
+ });
+
+ AboutProtectionsParent.setTestOverride(null);
+ Services.logins.removeLogin(TEST_LOGIN1);
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function testLockwiseCardPref() {
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ info("Disable showing the Lockwise card.");
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.report.lockwise.enabled",
+ false
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const lockwiseCard = content.document.querySelector(".lockwise-card");
+ await ContentTaskUtils.waitForCondition(() => {
+ return !lockwiseCard["data-enabled"];
+ }, "Lockwise card is not enabled.");
+
+ ok(ContentTaskUtils.isHidden(lockwiseCard), "Lockwise card is hidden.");
+ });
+
+ // Set the pref back to displaying the card.
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.report.lockwise.enabled",
+ true
+ );
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/protections/test/browser/browser_protections_monitor.js b/browser/components/protections/test/browser/browser_protections_monitor.js
new file mode 100644
index 0000000000..b24d8de55c
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_monitor.js
@@ -0,0 +1,161 @@
+/* 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 { AboutProtectionsParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutProtectionsParent.sys.mjs"
+);
+
+const monitorErrorData = {
+ error: true,
+};
+
+const mockMonitorData = {
+ numBreaches: 11,
+ numBreachesResolved: 0,
+};
+
+add_task(async function () {
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ await BrowserTestUtils.reloadTab(tab);
+
+ const monitorCardEnabled = Services.prefs.getBoolPref(
+ "browser.contentblocking.report.monitor.enabled"
+ );
+
+ // Only run monitor card tests if it's enabled.
+ if (monitorCardEnabled) {
+ info(
+ "Check that the correct content is displayed for users with no logins."
+ );
+ await checkNoLoginsContentIsDisplayed(tab, "monitor-sign-up");
+
+ info(
+ "Check that the correct content is displayed for users with monitor data."
+ );
+ await Services.logins.addLoginAsync(TEST_LOGIN1);
+ AboutProtectionsParent.setTestOverride(mockGetMonitorData(mockMonitorData));
+ await BrowserTestUtils.reloadTab(tab);
+
+ Assert.ok(
+ true,
+ "Error was not thrown for trying to reach the Monitor endpoint, the cache has worked."
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ const hasLogins = content.document.querySelector(
+ ".monitor-card.has-logins"
+ );
+ return hasLogins && ContentTaskUtils.isVisible(hasLogins);
+ }, "Monitor card for user with stored logins is shown.");
+
+ const hasLoginsHeaderContent = content.document.querySelector(
+ "#monitor-header-content span"
+ );
+ const cardBody = content.document.querySelector(
+ ".monitor-card .card-body"
+ );
+
+ ok(
+ ContentTaskUtils.isVisible(cardBody),
+ "Card body is shown for users monitor data."
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ hasLoginsHeaderContent.textContent ==
+ "Firefox Monitor warns you if your info has appeared in a known data breach."
+ );
+ }, "Header content for user with monitor data is correct.");
+
+ info("Make sure correct numbers for monitor stats are displayed.");
+ const emails = content.document.querySelector(
+ ".monitor-stat span[data-type='stored-emails']"
+ );
+ const passwords = content.document.querySelector(
+ ".monitor-stat span[data-type='exposed-passwords']"
+ );
+ const breaches = content.document.querySelector(
+ ".monitor-stat span[data-type='known-breaches']"
+ );
+
+ is(emails.textContent, 1, "1 monitored email is displayed");
+ is(passwords.textContent, 8, "8 exposed passwords are displayed");
+ is(breaches.textContent, 11, "11 known data breaches are displayed.");
+ });
+
+ info(
+ "Check that correct content is displayed when monitor data contains an error message."
+ );
+ AboutProtectionsParent.setTestOverride(
+ mockGetMonitorData(monitorErrorData)
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await checkNoLoginsContentIsDisplayed(tab);
+
+ info("Disable showing the Monitor card.");
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.report.monitor.enabled",
+ false
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ }
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ const monitorCard = content.document.querySelector(".monitor-card");
+ return !monitorCard["data-enabled"];
+ }, "Monitor card is not enabled.");
+
+ const monitorCard = content.document.querySelector(".monitor-card");
+ ok(ContentTaskUtils.isHidden(monitorCard), "Monitor card is hidden.");
+ });
+
+ if (monitorCardEnabled) {
+ // set the pref back to displaying the card.
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.report.monitor.enabled",
+ true
+ );
+
+ // remove logins
+ Services.logins.removeLogin(TEST_LOGIN1);
+
+ // restore original test functions
+ AboutProtectionsParent.setTestOverride(null);
+ }
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+async function checkNoLoginsContentIsDisplayed(tab, expectedLinkContent) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ const noLogins = content.document.querySelector(
+ ".monitor-card.no-logins"
+ );
+ return noLogins && ContentTaskUtils.isVisible(noLogins);
+ }, "Monitor card for user with no logins is shown.");
+
+ const noLoginsHeaderContent = content.document.querySelector(
+ "#monitor-header-content span"
+ );
+ const cardBody = content.document.querySelector(".monitor-card .card-body");
+
+ ok(
+ ContentTaskUtils.isHidden(cardBody),
+ "Card body is hidden for users with no logins."
+ );
+ is(
+ noLoginsHeaderContent.getAttribute("data-l10n-id"),
+ "monitor-header-content-no-account",
+ "Header content for user with no logins is correct"
+ );
+ });
+}
diff --git a/browser/components/protections/test/browser/browser_protections_proxy.js b/browser/components/protections/test/browser/browser_protections_proxy.js
new file mode 100644
index 0000000000..b5d5e396f5
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_proxy.js
@@ -0,0 +1,107 @@
+/* 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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Region: "resource://gre/modules/Region.sys.mjs",
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.vpn_promo.enabled", false],
+ ],
+ });
+});
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ info("Secure Proxy card should be hidden by default");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ const proxyCard = content.document.querySelector(".proxy-card");
+ return !proxyCard["data-enabled"];
+ }, "Proxy card is not enabled.");
+
+ const proxyCard = content.document.querySelector(".proxy-card");
+ ok(ContentTaskUtils.isHidden(proxyCard), "Proxy card is hidden.");
+ });
+
+ info("Enable showing the Secure Proxy card");
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.report.proxy.enabled",
+ true
+ );
+
+ info(
+ "Check that secure proxy card is hidden if user's language is not en-US"
+ );
+ Services.prefs.setCharPref("intl.accept_languages", "en-CA");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkProxyCardVisibility(tab, true);
+
+ info(
+ "Check that secure proxy card is shown if user's location is in the US."
+ );
+ // Set language back to en-US
+ Services.prefs.setCharPref("intl.accept_languages", "en-US");
+ Region._setHomeRegion("US", false);
+ await BrowserTestUtils.reloadTab(tab);
+ await checkProxyCardVisibility(tab, false);
+
+ info(
+ "Check that secure proxy card is hidden if user's location is not in the US."
+ );
+ Region._setHomeRegion("CA", false);
+ await BrowserTestUtils.reloadTab(tab);
+ await checkProxyCardVisibility(tab, true);
+
+ info(
+ "Check that secure proxy card is hidden if the extension is already installed."
+ );
+ // Make sure we set the region back to "US"
+ Region._setHomeRegion("US", false);
+ const id = "secure-proxy@mozilla.com";
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ name: "Firefox Proxy",
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ await BrowserTestUtils.reloadTab(tab);
+ await checkProxyCardVisibility(tab, true);
+ await extension.unload();
+
+ Services.prefs.setBoolPref(
+ "browser.contentblocking.report.proxy.enabled",
+ false
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+async function checkProxyCardVisibility(tab, shouldBeHidden) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ _shouldBeHidden: shouldBeHidden }],
+ async function ({ _shouldBeHidden }) {
+ await ContentTaskUtils.waitForCondition(() => {
+ const proxyCard = content.document.querySelector(".proxy-card");
+ return ContentTaskUtils.isHidden(proxyCard) === _shouldBeHidden;
+ });
+
+ const visibilityState = _shouldBeHidden ? "hidden" : "shown";
+ ok(true, `Proxy card is ${visibilityState}.`);
+ }
+ );
+}
diff --git a/browser/components/protections/test/browser/browser_protections_report_ui.js b/browser/components/protections/test/browser/browser_protections_report_ui.js
new file mode 100644
index 0000000000..bc099a10a9
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_report_ui.js
@@ -0,0 +1,1129 @@
+/* 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/. */
+
+// Note: This test may cause intermittents if run at exactly midnight.
+
+const { Sqlite } = ChromeUtils.importESModule(
+ "resource://gre/modules/Sqlite.sys.mjs"
+);
+const { AboutProtectionsParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutProtectionsParent.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+ChromeUtils.defineLazyGetter(this, "DB_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
+});
+
+const SQL = {
+ insertCustomTimeEvent:
+ "INSERT INTO events (type, count, timestamp)" +
+ "VALUES (:type, :count, date(:timestamp));",
+
+ selectAll: "SELECT * FROM events",
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.database.enabled", true],
+ ["browser.vpn_promo.enabled", false],
+ ],
+ });
+});
+
+add_task(async function test_graph_display() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let date = new Date().toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 4,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ date = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 4,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 4,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ date = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ date = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ date = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 8,
+ timestamp: date,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const DATA_TYPES = [
+ "cryptominer",
+ "fingerprinter",
+ "tracker",
+ "cookie",
+ "social",
+ ];
+ let allBars = null;
+ await ContentTaskUtils.waitForCondition(() => {
+ allBars = content.document.querySelectorAll(".graph-bar");
+ return allBars.length;
+ }, "The graph has been built");
+
+ Assert.equal(allBars.length, 7, "7 bars have been found on the graph");
+
+ // For accessibility, test if the graph is a table
+ // and has a correct column count (number of data types + total + day)
+ Assert.equal(
+ content.document.getElementById("graph").getAttribute("role"),
+ "table",
+ "Graph is an accessible table"
+ );
+ Assert.equal(
+ content.document.getElementById("graph").getAttribute("aria-colcount"),
+ DATA_TYPES.length + 2,
+ "Table has the right number of columns"
+ );
+ Assert.equal(
+ content.document.getElementById("graph").getAttribute("aria-labelledby"),
+ "graphLegendDescription",
+ "Table has an accessible label"
+ );
+
+ // today has each type
+ // yesterday will have no tracking cookies
+ // 2 days ago will have no fingerprinters
+ // 3 days ago will have no cryptominers
+ // 4 days ago will have no trackers
+ // 5 days ago will have no social (when we add social)
+ // 6 days ago will be empty
+ Assert.equal(
+ allBars[6].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length,
+ "today has all of the data types shown"
+ );
+ Assert.equal(
+ allBars[6].getAttribute("role"),
+ "row",
+ "Today has the correct role"
+ );
+ Assert.equal(
+ allBars[6].getAttribute("aria-owns"),
+ "day0 count0 cryptominer0 fingerprinter0 tracker0 cookie0 social0",
+ "Row has the columns in the right order"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".tracker-bar").style.height,
+ "10%",
+ "trackers take 10%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".tracker-bar").parentNode.getAttribute("role"),
+ "cell",
+ "Trackers have the correct role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".tracker-bar").getAttribute("role"),
+ "img",
+ "Tracker bar has the correct image role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".tracker-bar").getAttribute("aria-label"),
+ "1 tracking content (10%)",
+ "Trackers have the correct accessible text"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cryptominer-bar").style.height,
+ "20%",
+ "cryptominers take 20%"
+ );
+ Assert.equal(
+ allBars[6]
+ .querySelector(".cryptominer-bar")
+ .parentNode.getAttribute("role"),
+ "cell",
+ "Cryptominers have the correct role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cryptominer-bar").getAttribute("role"),
+ "img",
+ "Cryptominer bar has the correct image role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cryptominer-bar").getAttribute("aria-label"),
+ "2 cryptominers (20%)",
+ "Cryptominers have the correct accessible label"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".fingerprinter-bar").style.height,
+ "20%",
+ "fingerprinters take 20%"
+ );
+ Assert.equal(
+ allBars[6]
+ .querySelector(".fingerprinter-bar")
+ .parentNode.getAttribute("role"),
+ "cell",
+ "Fingerprinters have the correct role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".fingerprinter-bar").getAttribute("role"),
+ "img",
+ "Fingerprinter bar has the correct image role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".fingerprinter-bar").getAttribute("aria-label"),
+ "2 fingerprinters (20%)",
+ "Fingerprinters have the correct accessible label"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cookie-bar").style.height,
+ "40%",
+ "cross site tracking cookies take 40%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cookie-bar").parentNode.getAttribute("role"),
+ "cell",
+ "cross site tracking cookies have the correct role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cookie-bar").getAttribute("role"),
+ "img",
+ "Cross site tracking cookies bar has the correct image role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cookie-bar").getAttribute("aria-label"),
+ "4 cross-site tracking cookies (40%)",
+ "cross site tracking cookies have the correct accessible label"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".social-bar").style.height,
+ "10%",
+ "social trackers take 10%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".social-bar").parentNode.getAttribute("role"),
+ "cell",
+ "social trackers have the correct role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".social-bar").getAttribute("role"),
+ "img",
+ "social tracker bar has the correct image role"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".social-bar").getAttribute("aria-label"),
+ "1 social media tracker (10%)",
+ "social trackers have the correct accessible text"
+ );
+
+ Assert.equal(
+ allBars[5].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length - 1,
+ "1 day ago is missing one type"
+ );
+ Assert.ok(
+ !allBars[5].querySelector(".cookie-bar"),
+ "there is no cross site tracking cookie section 1 day ago."
+ );
+ Assert.equal(
+ allBars[5].getAttribute("aria-owns"),
+ "day1 count1 cryptominer1 fingerprinter1 tracker1 social1",
+ "Row has the columns in the right order"
+ );
+
+ Assert.equal(
+ allBars[4].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length - 1,
+ "2 days ago is missing one type"
+ );
+ Assert.ok(
+ !allBars[4].querySelector(".fingerprinter-bar"),
+ "there is no fingerprinter section 1 day ago."
+ );
+ Assert.equal(
+ allBars[4].getAttribute("aria-owns"),
+ "day2 count2 cryptominer2 tracker2 cookie2 social2",
+ "Row has the columns in the right order"
+ );
+
+ Assert.equal(
+ allBars[3].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length - 1,
+ "3 days ago is missing one type"
+ );
+ Assert.ok(
+ !allBars[3].querySelector(".cryptominer-bar"),
+ "there is no cryptominer section 1 day ago."
+ );
+ Assert.equal(
+ allBars[3].getAttribute("aria-owns"),
+ "day3 count3 fingerprinter3 tracker3 cookie3 social3",
+ "Row has the columns in the right order"
+ );
+
+ Assert.equal(
+ allBars[2].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length - 1,
+ "4 days ago is missing one type"
+ );
+ Assert.ok(
+ !allBars[2].querySelector(".tracker-bar"),
+ "there is no tracker section 1 day ago."
+ );
+ Assert.equal(
+ allBars[2].getAttribute("aria-owns"),
+ "day4 count4 cryptominer4 fingerprinter4 cookie4 social4",
+ "Row has the columns in the right order"
+ );
+
+ Assert.equal(
+ allBars[1].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length - 1,
+ "5 days ago is missing one type"
+ );
+ Assert.ok(
+ !allBars[1].querySelector(".social-bar"),
+ "there is no social section 1 day ago."
+ );
+ Assert.equal(
+ allBars[1].getAttribute("aria-owns"),
+ "day5 count5 cryptominer5 fingerprinter5 tracker5 cookie5",
+ "Row has the columns in the right order"
+ );
+
+ Assert.equal(
+ allBars[0].querySelectorAll(".inner-bar").length,
+ 0,
+ "6 days ago has no content"
+ );
+ Assert.ok(
+ allBars[0].classList.contains("empty"),
+ "6 days ago is an empty bar"
+ );
+ Assert.equal(
+ allBars[0].getAttribute("aria-owns"),
+ "day6 ",
+ "Row has the columns in the right order"
+ );
+
+ // Check that each tab has the correct aria-labelledby and aria-describedby
+ // values. This helps screen readers know what type of tracker the reported
+ // tab number is referencing.
+ const socialTab = content.document.getElementById("tab-social");
+ Assert.equal(
+ socialTab.getAttribute("aria-labelledby"),
+ "socialLabel socialTitle",
+ "aria-labelledby attribute is socialLabel socialTitle"
+ );
+ Assert.equal(
+ socialTab.getAttribute("aria-describedby"),
+ "socialContent",
+ "aria-describedby attribute is socialContent"
+ );
+
+ const cookieTab = content.document.getElementById("tab-cookie");
+ Assert.equal(
+ cookieTab.getAttribute("aria-labelledby"),
+ "cookieLabel cookieTitle",
+ "aria-labelledby attribute is cookieLabel cookieTitle"
+ );
+ Assert.equal(
+ cookieTab.getAttribute("aria-describedby"),
+ "cookieContent",
+ "aria-describedby attribute is cookieContent"
+ );
+
+ const trackerTab = content.document.getElementById("tab-tracker");
+ Assert.equal(
+ trackerTab.getAttribute("aria-labelledby"),
+ "trackerLabel trackerTitle",
+ "aria-labelledby attribute is trackerLabel trackerTitle"
+ );
+ Assert.equal(
+ trackerTab.getAttribute("aria-describedby"),
+ "trackerContent",
+ "aria-describedby attribute is trackerContent"
+ );
+
+ const fingerprinterTab =
+ content.document.getElementById("tab-fingerprinter");
+ Assert.equal(
+ fingerprinterTab.getAttribute("aria-labelledby"),
+ "fingerprinterLabel fingerprinterTitle",
+ "aria-labelledby attribute is fingerprinterLabel fingerprinterTitle"
+ );
+ Assert.equal(
+ fingerprinterTab.getAttribute("aria-describedby"),
+ "fingerprinterContent",
+ "aria-describedby attribute is fingerprinterContent"
+ );
+
+ const cryptominerTab = content.document.getElementById("tab-cryptominer");
+ Assert.equal(
+ cryptominerTab.getAttribute("aria-labelledby"),
+ "cryptominerLabel cryptominerTitle",
+ "aria-labelledby attribute is cryptominerLabel cryptominerTitle"
+ );
+ Assert.equal(
+ cryptominerTab.getAttribute("aria-describedby"),
+ "cryptominerContent",
+ "aria-describedby attribute is cryptominerContent"
+ );
+ });
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ let rows = await db.execute(SQL.selectAll);
+ is(rows.length, 0, "length is 0");
+ await db.close();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Ensure that the number of suspicious fingerprinter is aggregated into the
+// fingerprinter category on about:protection page.
+add_task(async function test_suspicious_fingerprinter() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ // Inserting data for today. It won't contain a fingerprinter entry but only
+ // a suspicious fingerprinter entry.
+ let date = new Date().toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 4,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ // Inserting data for 1 day age. It contains both a fingerprinter entry and
+ // a suspicious fingerprinter entry.
+ date = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 4,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const DATA_TYPES = [
+ "cryptominer",
+ "fingerprinter",
+ "tracker",
+ "cookie",
+ "social",
+ ];
+ let allBars = null;
+ await ContentTaskUtils.waitForMutationCondition(
+ content.document.body,
+ { childList: true, subtree: true },
+ () => {
+ allBars = content.document.querySelectorAll(".graph-bar");
+ return !!allBars.length;
+ }
+ );
+ info("The graph has been built");
+
+ Assert.equal(allBars.length, 7, "7 bars have been found on the graph");
+
+ // Verify today's data. The fingerprinter category should take 20%.
+ Assert.equal(
+ allBars[6].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length,
+ "today has all of the data types shown"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".tracker-bar").style.height,
+ "10%",
+ "trackers take 10%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cryptominer-bar").style.height,
+ "20%",
+ "cryptominers take 20%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".fingerprinter-bar").style.height,
+ "20%",
+ "fingerprinters take 20%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cookie-bar").style.height,
+ "40%",
+ "cross site tracking cookies take 40%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".social-bar").style.height,
+ "10%",
+ "social trackers take 10%"
+ );
+
+ // Verify one day age data. The fingerprinter category should take 20%.
+ Assert.equal(
+ allBars[5].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length,
+ "today has all of the data types shown"
+ );
+ Assert.equal(
+ allBars[5].querySelector(".tracker-bar").style.height,
+ "10%",
+ "trackers take 10%"
+ );
+ Assert.equal(
+ allBars[5].querySelector(".cryptominer-bar").style.height,
+ "20%",
+ "cryptominers take 20%"
+ );
+ Assert.equal(
+ allBars[5].querySelector(".fingerprinter-bar").style.height,
+ "20%",
+ "fingerprinters take 20%"
+ );
+ Assert.equal(
+ allBars[5].querySelector(".cookie-bar").style.height,
+ "40%",
+ "cross site tracking cookies take 40%"
+ );
+ Assert.equal(
+ allBars[5].querySelector(".social-bar").style.height,
+ "10%",
+ "social trackers take 10%"
+ );
+ });
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ let rows = await db.execute(SQL.selectAll);
+ is(rows.length, 0, "length is 0");
+ await db.close();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Ensure that the number of suspicious fingerprinter is displayed even if the
+// fingerprinter blocking is disabled.
+add_task(async function test_suspicious_fingerprinter_without_fp_blocking() {
+ // Disable fingerprinter blocking
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ false
+ );
+
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ // Inserting data for today. It won't contain a fingerprinter entry but only
+ // a suspicious fingerprinter entry.
+ let date = new Date().toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 4,
+ timestamp: date,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.SOCIAL_ID,
+ count: 1,
+ timestamp: date,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const DATA_TYPES = [
+ "cryptominer",
+ "fingerprinter",
+ "tracker",
+ "cookie",
+ "social",
+ ];
+ let allBars = null;
+ await ContentTaskUtils.waitForMutationCondition(
+ content.document.body,
+ { childList: true, subtree: true },
+ () => {
+ allBars = content.document.querySelectorAll(".graph-bar");
+ return !!allBars.length;
+ }
+ );
+ info("The graph has been built");
+
+ Assert.equal(allBars.length, 7, "7 bars have been found on the graph");
+
+ // Verify today's data. The fingerprinter category should take 20%.
+ Assert.equal(
+ allBars[6].querySelectorAll(".inner-bar").length,
+ DATA_TYPES.length,
+ "today has all of the data types shown"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".tracker-bar").style.height,
+ "10%",
+ "trackers take 10%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cryptominer-bar").style.height,
+ "20%",
+ "cryptominers take 20%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".fingerprinter-bar").style.height,
+ "20%",
+ "fingerprinters take 20%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".cookie-bar").style.height,
+ "40%",
+ "cross site tracking cookies take 40%"
+ );
+ Assert.equal(
+ allBars[6].querySelector(".social-bar").style.height,
+ "10%",
+ "social trackers take 10%"
+ );
+ });
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ let rows = await db.execute(SQL.selectAll);
+ is(rows.length, 0, "length is 0");
+ await db.close();
+ BrowserTestUtils.removeTab(tab);
+
+ Services.prefs.clearUserPref(
+ "privacy.trackingprotection.fingerprinting.enabled"
+ );
+});
+
+// Ensure that each type of tracker is hidden from the graph if there are no recorded
+// trackers of that type and the user has chosen to not block that type.
+add_task(async function test_etp_custom_settings() {
+ Services.prefs.setStringPref("browser.contentblocking.category", "strict");
+ Services.prefs.setBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ true
+ );
+ // hide cookies from the graph
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let legend = content.document.getElementById("legend");
+ return ContentTaskUtils.isVisible(legend);
+ }, "The legend is visible");
+
+ let label = content.document.getElementById("cookieLabel");
+ Assert.ok(ContentTaskUtils.isHidden(label), "Cookie Label is hidden");
+
+ label = content.document.getElementById("trackerLabel");
+ Assert.ok(ContentTaskUtils.isVisible(label), "Tracker Label is visible");
+ label = content.document.getElementById("socialLabel");
+ Assert.ok(ContentTaskUtils.isVisible(label), "Social Label is visible");
+ label = content.document.getElementById("cryptominerLabel");
+ Assert.ok(
+ ContentTaskUtils.isVisible(label),
+ "Cryptominer Label is visible"
+ );
+ label = content.document.getElementById("fingerprinterLabel");
+ Assert.ok(
+ ContentTaskUtils.isVisible(label),
+ "Fingerprinter Label is visible"
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // hide ad trackers from the graph
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let legend = content.document.getElementById("legend");
+ return ContentTaskUtils.isVisible(legend);
+ }, "The legend is visible");
+
+ let label = content.document.querySelector("#trackerLabel");
+ Assert.ok(ContentTaskUtils.isHidden(label), "Tracker Label is hidden");
+
+ label = content.document.querySelector("#socialLabel");
+ Assert.ok(ContentTaskUtils.isHidden(label), "Social Label is hidden");
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // hide social from the graph
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.socialtracking.enabled",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ false
+ );
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let legend = content.document.getElementById("legend");
+ return ContentTaskUtils.isVisible(legend);
+ }, "The legend is visible");
+
+ let label = content.document.querySelector("#socialLabel");
+ Assert.ok(ContentTaskUtils.isHidden(label), "Social Label is hidden");
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // hide fingerprinting from the graph
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ false
+ );
+ Services.prefs.setBoolPref("privacy.fingerprintingProtection", false);
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let legend = content.document.getElementById("legend");
+ return ContentTaskUtils.isVisible(legend);
+ }, "The legend is visible");
+
+ let label = content.document.querySelector("#fingerprinterLabel");
+ Assert.ok(
+ ContentTaskUtils.isHidden(label),
+ "Fingerprinter Label is hidden"
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // hide cryptomining from the graph
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.cryptomining.enabled",
+ false
+ );
+ // Turn fingerprinting on so that all protectionsare not turned off, otherwise we will get a special card.
+ Services.prefs.setBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ true
+ );
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let legend = content.document.getElementById("legend");
+ return ContentTaskUtils.isVisible(legend);
+ }, "The legend is visible");
+
+ let label = content.document.querySelector("#cryptominerLabel");
+ Assert.ok(ContentTaskUtils.isHidden(label), "Cryptominer Label is hidden");
+ });
+ Services.prefs.clearUserPref("browser.contentblocking.category");
+ Services.prefs.clearUserPref(
+ "privacy.trackingprotection.fingerprinting.enabled"
+ );
+ Services.prefs.clearUserPref("privacy.fingerprintingProtection");
+ Services.prefs.clearUserPref(
+ "privacy.trackingprotection.cryptomining.enabled"
+ );
+ Services.prefs.clearUserPref("privacy.trackingprotection.enabled");
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+ Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Ensure that the Custom manage Protections card is shown if the user has all protections turned off.
+add_task(async function test_etp_custom_protections_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.category", "custom"],
+ ["network.cookie.cookieBehavior", 0], // not blocking
+ ["privacy.trackingprotection.cryptomining.enabled", false], // not blocking
+ ["privacy.trackingprotection.fingerprinting.enabled", false],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.socialtracking.enabled", false],
+ ["privacy.socialtracking.block_cookies.enabled", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ let aboutPreferencesPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let etpCard = content.document.querySelector(".etp-card");
+ return etpCard.classList.contains("custom-not-blocking");
+ }, "The custom protections warning card is showing");
+
+ let manageProtectionsButton =
+ content.document.getElementById("manage-protections");
+ Assert.ok(
+ ContentTaskUtils.isVisible(manageProtectionsButton),
+ "Button to manage protections is displayed"
+ );
+ });
+
+ // Custom protection card should show, even if there would otherwise be data on the graph.
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ let date = new Date().toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 1,
+ timestamp: date,
+ });
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let etpCard = content.document.querySelector(".etp-card");
+ return etpCard.classList.contains("custom-not-blocking");
+ }, "The custom protections warning card is showing");
+
+ let manageProtectionsButton =
+ content.document.getElementById("manage-protections");
+ Assert.ok(
+ ContentTaskUtils.isVisible(manageProtectionsButton),
+ "Button to manage protections is displayed"
+ );
+
+ manageProtectionsButton.click();
+ });
+ let aboutPreferencesTab = await aboutPreferencesPromise;
+ info("about:preferences#privacy was successfully opened in a new tab");
+ gBrowser.removeTab(aboutPreferencesTab);
+
+ Services.prefs.setStringPref("browser.contentblocking.category", "standard");
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ let rows = await db.execute(SQL.selectAll);
+ is(rows.length, 0, "length is 0");
+ await db.close();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Ensure that the ETP mobile promotion card is shown when the pref is on and
+// there are no mobile devices connected.
+add_task(async function test_etp_mobile_promotion_pref_on() {
+ AboutProtectionsParent.setTestOverride(mockGetLoginDataWithSyncedDevices());
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.contentblocking.report.show_mobile_app", true]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let mobilePromotion = content.document.getElementById("mobile-hanger");
+ Assert.ok(
+ ContentTaskUtils.isVisible(mobilePromotion),
+ "Mobile promotions card is displayed when pref is on and there are no synced mobile devices"
+ );
+
+ // Card should hide after the X is clicked.
+ mobilePromotion.querySelector(".exit-icon").click();
+ Assert.ok(
+ ContentTaskUtils.isHidden(mobilePromotion),
+ "Mobile promotions card is no longer displayed after clicking the X button"
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+
+ // Add a mock mobile device. The promotion should now be hidden.
+ AboutProtectionsParent.setTestOverride(
+ mockGetLoginDataWithSyncedDevices(true)
+ );
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let mobilePromotion = content.document.getElementById("mobile-hanger");
+ Assert.ok(
+ ContentTaskUtils.isHidden(mobilePromotion),
+ "Mobile promotions card is hidden when pref is on if there are synced mobile devices"
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ AboutProtectionsParent.setTestOverride(null);
+});
+
+// Test that ETP mobile promotion is not shown when the pref is off,
+// even if no mobile devices are synced.
+add_task(async function test_etp_mobile_promotion_pref_on() {
+ AboutProtectionsParent.setTestOverride(mockGetLoginDataWithSyncedDevices());
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.contentblocking.report.show_mobile_app", false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let mobilePromotion = content.document.getElementById("mobile-hanger");
+ Assert.ok(
+ ContentTaskUtils.isHidden(mobilePromotion),
+ "Mobile promotions card is not displayed when pref is off and there are no synced mobile devices"
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ AboutProtectionsParent.setTestOverride(
+ mockGetLoginDataWithSyncedDevices(true)
+ );
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let mobilePromotion = content.document.getElementById("mobile-hanger");
+ Assert.ok(
+ ContentTaskUtils.isHidden(mobilePromotion),
+ "Mobile promotions card is not displayed when pref is off even if there are synced mobile devices"
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+ AboutProtectionsParent.setTestOverride(null);
+});
+
+// Test that clicking on the link to settings in the header properly opens the settings page.
+add_task(async function test_settings_links() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+ let aboutPreferencesPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const protectionSettings = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("protection-settings");
+ }, "protection-settings link exists");
+
+ protectionSettings.click();
+ });
+ let aboutPreferencesTab = await aboutPreferencesPromise;
+ info("about:preferences#privacy was successfully opened in a new tab");
+ gBrowser.removeTab(aboutPreferencesTab);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/protections/test/browser/browser_protections_telemetry.js b/browser/components/protections/test/browser/browser_protections_telemetry.js
new file mode 100644
index 0000000000..2073be23e9
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_telemetry.js
@@ -0,0 +1,1123 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+const { AboutProtectionsParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutProtectionsParent.sys.mjs"
+);
+
+const LOG = {
+ "https://1.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
+ ],
+ "https://2.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+ "https://3.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2],
+ ],
+ "https://4.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3],
+ ],
+ "https://5.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1],
+ ],
+ // Cookie blocked for other reason, then identified as a tracker
+ "https://6.example.com": [
+ [
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL |
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
+ true,
+ 4,
+ ],
+ ],
+};
+
+requestLongerTimeout(2);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"],
+ [
+ "browser.vpn_promo.disallowed_regions",
+ "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
+ ],
+
+ // Change the endpoints to prevent non-local network connections when landing on the page.
+ ["browser.contentblocking.report.monitor.url", ""],
+ ["browser.contentblocking.report.monitor.sign_in_url", ""],
+ ["browser.contentblocking.report.social.url", ""],
+ ["browser.contentblocking.report.cookie.url", ""],
+ ["browser.contentblocking.report.tracker.url", ""],
+ ["browser.contentblocking.report.fingerprinter.url", ""],
+ ["browser.contentblocking.report.cryptominer.url", ""],
+ ["browser.contentblocking.report.mobile-ios.url", ""],
+ ["browser.contentblocking.report.mobile-android.url", ""],
+ ["browser.contentblocking.report.monitor.home_page_url", ""],
+ ["browser.contentblocking.report.monitor.preferences_url", ""],
+ ["browser.contentblocking.report.vpn.url", ""],
+ ["browser.contentblocking.report.vpn-promo.url", ""],
+ ["browser.contentblocking.report.vpn-android.url", ""],
+ ["browser.contentblocking.report.vpn-ios.url", ""],
+ ],
+ });
+
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ registerCleanupFunction(() => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ // AboutProtectionsParent.setTestOverride(null);
+ });
+});
+
+add_task(async function checkTelemetryLoadEvents() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.database.enabled", false],
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["browser.vpn_promo.enabled", false],
+ ],
+ });
+ await addArbitraryTimeout();
+
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+
+ Services.telemetry.setEventRecordingEnabled("security.ui.protections", true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ let loadEvents = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ if (events && events.length) {
+ events = events.filter(
+ e => e[1] == "security.ui.protections" && e[2] == "show"
+ );
+ if (events.length == 1) {
+ return events;
+ }
+ }
+ return null;
+ }, "recorded telemetry for showing the report");
+
+ is(loadEvents.length, 1, `recorded telemetry for showing the report`);
+ await BrowserTestUtils.reloadTab(tab);
+ loadEvents = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ if (events && events.length) {
+ events = events.filter(
+ e => e[1] == "security.ui.protections" && e[2] == "close"
+ );
+ if (events.length == 1) {
+ return events;
+ }
+ }
+ return null;
+ }, "recorded telemetry for closing the report");
+
+ is(loadEvents.length, 1, `recorded telemetry for closing the report`);
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+function waitForTelemetryEventCount(count) {
+ info("waiting for telemetry event count of " + count);
+ return TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).content;
+ if (!events) {
+ return null;
+ }
+ // Ignore irrelevant events from other parts of the browser.
+ events = events.filter(e => e[1] == "security.ui.protections");
+ info("got " + (events && events.length) + " events");
+ if (events.length == count) {
+ return events;
+ }
+ return null;
+ }, "waiting for telemetry event count of: " + count);
+}
+
+let addArbitraryTimeout = async () => {
+ // There's an arbitrary interval of 2 seconds in which the content
+ // processes sync their event data with the parent process, we wait
+ // this out to ensure that we clear everything that is left over from
+ // previous tests and don't receive random events in the middle of our tests.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 2000));
+};
+
+add_task(async function checkTelemetryClickEvents() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.database.enabled", true],
+ ["browser.contentblocking.report.monitor.enabled", true],
+ ["browser.contentblocking.report.lockwise.enabled", true],
+ ["browser.contentblocking.report.proxy.enabled", true],
+ ["browser.vpn_promo.enabled", false],
+ ],
+ });
+ await addArbitraryTimeout();
+
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+
+ Services.telemetry.setEventRecordingEnabled("security.ui.protections", true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ // Add user logins.
+ await Services.logins.addLoginAsync(TEST_LOGIN1);
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const managePasswordsButton = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("manage-passwords-button");
+ },
+ "Manage passwords button exists"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(managePasswordsButton),
+ "manage passwords button is visible"
+ );
+ managePasswordsButton.click();
+ });
+
+ let events = await waitForTelemetryEventCount(4);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "lw_open_button" &&
+ e[4] == "manage_passwords"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for lw_open_button when there are no breached passwords`
+ );
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Add breached logins.
+ AboutProtectionsParent.setTestOverride(
+ mockGetLoginDataWithSyncedDevices(false, 4)
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const managePasswordsButton = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("manage-passwords-button");
+ },
+ "Manage passwords button exists"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(managePasswordsButton),
+ "manage passwords button is visible"
+ );
+ managePasswordsButton.click();
+ });
+
+ events = await waitForTelemetryEventCount(7);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "lw_open_button" &&
+ e[4] == "manage_breached_passwords"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for lw_open_button when there are breached passwords`
+ );
+ AboutProtectionsParent.setTestOverride(null);
+ Services.logins.removeLogin(TEST_LOGIN1);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.reloadTab(tab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ // Show all elements, so we can click on them, even though our user is not logged in.
+ let hidden_elements = content.document.querySelectorAll(".hidden");
+ for (let el of hidden_elements) {
+ el.style.display = "block ";
+ }
+
+ const savePasswordsButton = await ContentTaskUtils.waitForCondition(() => {
+ // Opens an extra tab
+ return content.document.getElementById("save-passwords-button");
+ }, "Save Passwords button exists");
+
+ savePasswordsButton.click();
+ });
+
+ events = await waitForTelemetryEventCount(10);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "lw_open_button" &&
+ e[4] == "save_passwords"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for lw_open_button when there are no stored passwords`
+ );
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const lockwiseAboutLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("lockwise-how-it-works");
+ }, "lockwiseReportLink exists");
+
+ lockwiseAboutLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(11);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "lw_about_link"
+ );
+ is(events.length, 1, `recorded telemetry for lw_about_link`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let monitorAboutLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("monitor-link");
+ }, "monitorAboutLink exists");
+
+ monitorAboutLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(12);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_about_link"
+ );
+ is(events.length, 1, `recorded telemetry for mtr_about_link`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const signUpForMonitorLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("sign-up-for-monitor-link");
+ }, "signUpForMonitorLink exists");
+
+ signUpForMonitorLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(13);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_signup_button"
+ );
+ is(events.length, 1, `recorded telemetry for mtr_signup_button`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const socialLearnMoreLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("social-link");
+ }, "Learn more link for social tab exists");
+
+ socialLearnMoreLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(14);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "trackers_about_link" &&
+ e[4] == "social"
+ );
+ is(events.length, 1, `recorded telemetry for social trackers_about_link`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const cookieLearnMoreLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("cookie-link");
+ }, "Learn more link for cookie tab exists");
+
+ cookieLearnMoreLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(15);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "trackers_about_link" &&
+ e[4] == "cookie"
+ );
+ is(events.length, 1, `recorded telemetry for cookie trackers_about_link`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const trackerLearnMoreLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("tracker-link");
+ }, "Learn more link for tracker tab exists");
+
+ trackerLearnMoreLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(16);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "trackers_about_link" &&
+ e[4] == "tracker"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for content tracker trackers_about_link`
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const fingerprinterLearnMoreLink = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("fingerprinter-link");
+ },
+ "Learn more link for fingerprinter tab exists"
+ );
+
+ fingerprinterLearnMoreLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(17);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "trackers_about_link" &&
+ e[4] == "fingerprinter"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for fingerprinter trackers_about_link`
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const cryptominerLearnMoreLink = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("cryptominer-link");
+ },
+ "Learn more link for cryptominer tab exists"
+ );
+
+ cryptominerLearnMoreLink.click();
+ });
+
+ events = await waitForTelemetryEventCount(18);
+
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "trackers_about_link" &&
+ e[4] == "cryptominer"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for cryptominer trackers_about_link`
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const protectionSettings = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("protection-settings");
+ }, "protection-settings link exists");
+
+ protectionSettings.click();
+ });
+
+ events = await waitForTelemetryEventCount(19);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "settings_link" &&
+ e[4] == "header-settings"
+ );
+ is(events.length, 1, `recorded telemetry for settings_link header-settings`);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const customProtectionSettings = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("manage-protections");
+ },
+ "manage-protections link exists"
+ );
+ // Show element so we can click on it
+ customProtectionSettings.style.display = "block";
+
+ customProtectionSettings.click();
+ });
+
+ events = await waitForTelemetryEventCount(20);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "settings_link" &&
+ e[4] == "custom-card-settings"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for settings_link custom-card-settings`
+ );
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // Add breached logins and some resolved breaches.
+ AboutProtectionsParent.setTestOverride(
+ mockGetMonitorData({
+ potentiallyBreachedLogins: 4,
+ numBreaches: 3,
+ numBreachesResolved: 1,
+ })
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const resolveBreachesButton = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("monitor-partial-breaches-link");
+ },
+ "Monitor resolve breaches button exists"
+ );
+
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(resolveBreachesButton),
+ "Resolve breaches button is visible"
+ );
+
+ resolveBreachesButton.click();
+ });
+
+ events = await waitForTelemetryEventCount(23);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "resolve_breaches"
+ );
+ is(events.length, 1, `recorded telemetry for resolve breaches button`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const monitorKnownBreachesBlock = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("monitor-known-breaches-link");
+ },
+ "Monitor card known breaches block exists"
+ );
+
+ monitorKnownBreachesBlock.click();
+ });
+
+ events = await waitForTelemetryEventCount(24);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "known_resolved_breaches"
+ );
+ is(events.length, 1, `recorded telemetry for monitor known breaches block`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const monitorExposedPasswordsBlock =
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById(
+ "monitor-exposed-passwords-link"
+ );
+ }, "Monitor card exposed passwords block exists");
+
+ monitorExposedPasswordsBlock.click();
+ });
+
+ events = await waitForTelemetryEventCount(25);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "exposed_passwords_unresolved_breaches"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for monitor exposed passwords block`
+ );
+
+ // Add breached logins and no resolved breaches.
+ AboutProtectionsParent.setTestOverride(
+ mockGetMonitorData({
+ potentiallyBreachedLogins: 4,
+ numBreaches: 3,
+ numBreachesResolved: 0,
+ })
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const manageBreachesButton = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("monitor-breaches-link");
+ }, "Monitor manage breaches button exists");
+
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(manageBreachesButton),
+ "Manage breaches button is visible"
+ );
+
+ manageBreachesButton.click();
+ });
+
+ events = await waitForTelemetryEventCount(28);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "manage_breaches"
+ );
+ is(events.length, 1, `recorded telemetry for manage breaches button`);
+
+ // All breaches are resolved.
+ AboutProtectionsParent.setTestOverride(
+ mockGetMonitorData({
+ potentiallyBreachedLogins: 4,
+ numBreaches: 3,
+ numBreachesResolved: 3,
+ })
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const viewReportButton = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("monitor-breaches-link");
+ }, "Monitor view report button exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(viewReportButton),
+ "View report button is visible"
+ );
+
+ viewReportButton.click();
+ });
+
+ events = await waitForTelemetryEventCount(31);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "view_report"
+ );
+ is(events.length, 1, `recorded telemetry for view report button`);
+
+ // No breaches are present.
+ AboutProtectionsParent.setTestOverride(
+ mockGetMonitorData({
+ potentiallyBreachedLogins: 4,
+ numBreaches: 0,
+ numBreachesResolved: 0,
+ })
+ );
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const viewReportButton = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("monitor-breaches-link");
+ }, "Monitor view report button exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(viewReportButton),
+ "View report button is visible"
+ );
+
+ viewReportButton.click();
+ });
+
+ events = await waitForTelemetryEventCount(34);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "view_report"
+ );
+ is(events.length, 2, `recorded telemetry for view report button`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const monitorEmailBlock = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("monitor-stored-emails-link");
+ }, "Monitor card email block exists");
+
+ monitorEmailBlock.click();
+ });
+
+ events = await waitForTelemetryEventCount(35);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "stored_emails"
+ );
+ is(events.length, 1, `recorded telemetry for monitor email block`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const monitorKnownBreachesBlock = await ContentTaskUtils.waitForCondition(
+ () => {
+ return content.document.getElementById("monitor-known-breaches-link");
+ },
+ "Monitor card known breaches block exists"
+ );
+
+ monitorKnownBreachesBlock.click();
+ });
+
+ events = await waitForTelemetryEventCount(36);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "known_unresolved_breaches"
+ );
+ is(events.length, 1, `recorded telemetry for monitor known breaches block`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const monitorExposedPasswordsBlock =
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById(
+ "monitor-exposed-passwords-link"
+ );
+ }, "Monitor card exposed passwords block exists");
+
+ monitorExposedPasswordsBlock.click();
+ });
+
+ events = await waitForTelemetryEventCount(37);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "mtr_report_link" &&
+ e[4] == "exposed_passwords_all_breaches"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for monitor exposed passwords block`
+ );
+
+ // Clean up.
+ AboutProtectionsParent.setTestOverride(null);
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// This tests that telemetry is sent when saveEvents is called.
+add_task(async function test_save_telemetry() {
+ // Clear all scalar telemetry.
+ Services.telemetry.clearScalars();
+
+ await TrackingDBService.saveEvents(JSON.stringify(LOG));
+
+ const scalars = Services.telemetry.getSnapshotForScalars(
+ "main",
+ false
+ ).parent;
+ is(scalars["contentblocking.trackers_blocked_count"], 6);
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+});
+
+// Test that telemetry is sent if entrypoint param is included,
+// and test that it is recorded as default if entrypoint param is not properly included
+add_task(async function checkTelemetryLoadEventForEntrypoint() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.database.enabled", false],
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["browser.vpn_promo.enabled", false],
+ ],
+ });
+ await addArbitraryTimeout();
+
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+
+ Services.telemetry.setEventRecordingEnabled("security.ui.protections", true);
+
+ info("Typo in 'entrypoint' should not be recorded");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections?entryPoint=newPage",
+ gBrowser,
+ });
+
+ let loadEvents = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ if (events && events.length) {
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "show" &&
+ e[4] == "direct"
+ );
+ if (events.length == 1) {
+ return events;
+ }
+ }
+ return null;
+ }, "recorded telemetry for showing the report contains default 'direct' entrypoint");
+
+ is(
+ loadEvents.length,
+ 1,
+ `recorded telemetry for showing the report contains default 'direct' entrypoint`
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections?entrypoint=page",
+ gBrowser,
+ });
+
+ loadEvents = await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ if (events && events.length) {
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" && e[2] == "show" && e[4] == "page"
+ );
+ if (events.length == 1) {
+ return events;
+ }
+ }
+ return null;
+ }, "recorded telemetry for showing the report contains correct entrypoint");
+
+ is(
+ loadEvents.length,
+ 1,
+ "recorded telemetry for showing the report contains correct entrypoint"
+ );
+
+ // Clean up.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// This test is skipping due to failures on try, it passes locally.
+// Test that telemetry is sent from the vpn card
+add_task(async function checkTelemetryClickEventsVPN() {
+ if (Services.sysinfo.getProperty("name") != "Windows_NT") {
+ ok(true, "User is on an unsupported platform, the vpn card will not show");
+ return;
+ }
+ await addArbitraryTimeout();
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+ Services.telemetry.setEventRecordingEnabled("security.ui.protections", true);
+
+ // user is not subscribed to VPN, and is in the us
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false, "us"));
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ [
+ "browser.vpn_promo.disallowed_regions",
+ "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
+ ],
+ ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"],
+ ["browser.contentblocking.database.enabled", false],
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["browser.contentblocking.report.hide_vpn_banner", true],
+ ["browser.contentblocking.report.vpn-android.url", ""],
+ ["browser.contentblocking.report.vpn-ios.url", ""],
+ ["browser.contentblocking.report.vpn.url", ""],
+ ],
+ });
+ Services.locale.availableLocales = ["en-US"];
+ Services.locale.requestedLocales = ["en-US"];
+ await promiseSetHomeRegion("US");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ info("checking for vpn link");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const getVPNLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("get-vpn-link");
+ }, "get vpn link exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(getVPNLink),
+ "get vpn link is visible"
+ );
+ EventUtils.sendMouseEvent(
+ { type: "click", button: 1 },
+ getVPNLink,
+ content
+ );
+ });
+
+ let events = await waitForTelemetryEventCount(2);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "vpn_card_link"
+ );
+ is(
+ events.length,
+ 1,
+ `recorded telemetry for vpn_card_link when user is not subscribed`
+ );
+
+ // User is subscribed to VPN
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(true, "us"));
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const androidVPNLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("vpn-google-playstore-link");
+ }, "android vpn link exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(androidVPNLink),
+ "android vpn link is visible"
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document
+ .querySelector(".vpn-card")
+ .classList.contains("subscribed");
+ }, "subscribed class is added to the vpn card");
+
+ EventUtils.sendMouseEvent(
+ { type: "click", button: 1 },
+ androidVPNLink,
+ content
+ );
+ });
+
+ events = await waitForTelemetryEventCount(5);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "vpn_app_link_android"
+ );
+ is(events.length, 1, `recorded telemetry for vpn_app_link_android link`);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const iosVPNLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("vpn-app-store-link");
+ }, "ios vpn link exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(iosVPNLink),
+ "ios vpn link is visible"
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document
+ .querySelector(".vpn-card")
+ .classList.contains("subscribed");
+ }, "subscribed class is added to the vpn card");
+
+ EventUtils.sendMouseEvent(
+ { type: "click", button: 1 },
+ iosVPNLink,
+ content
+ );
+ });
+
+ events = await waitForTelemetryEventCount(6);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "vpn_app_link_ios"
+ );
+ is(events.length, 1, `recorded telemetry for vpn_app_link_ios link`);
+
+ // Clean up.
+ await BrowserTestUtils.removeTab(tab);
+}).skip();
+
+// This test is skipping due to failures on try, it passes locally.
+// Test that telemetry is sent from the vpn banner
+add_task(async function checkTelemetryEventsVPNBanner() {
+ if (Services.sysinfo.getProperty("name") != "Windows_NT") {
+ ok(true, "User is on an unsupported platform, the vpn card will not show");
+ return;
+ }
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false, "us"));
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"],
+ [
+ "browser.vpn_promo.disallowed_regions",
+ "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
+ ],
+ ["browser.contentblocking.database.enabled", false],
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ["browser.contentblocking.report.hide_vpn_banner", false],
+ ["browser.contentblocking.report.vpn-promo.url", ""],
+ ],
+ });
+ await addArbitraryTimeout();
+
+ // The VPN banner only shows if the user is in en*
+ Services.locale.availableLocales = ["en-US"];
+ Services.locale.requestedLocales = ["en-US"];
+
+ // Clear everything.
+ Services.telemetry.clearEvents();
+ await TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+
+ Services.telemetry.setEventRecordingEnabled("security.ui.protections", true);
+ // User is not subscribed to VPN
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false, "us"));
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const bannerVPNLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.getElementById("vpn-banner-link");
+ }, "vpn banner link exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(bannerVPNLink),
+ "vpn banner link is visible"
+ );
+ EventUtils.sendMouseEvent(
+ { type: "click", button: 1 },
+ bannerVPNLink,
+ content
+ );
+ });
+
+ let events = await waitForTelemetryEventCount(3);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "vpn_banner_link"
+ );
+ is(events.length, 1, `recorded telemetry for vpn_banner_link`);
+
+ // VPN Banner flips this pref each time it shows, flip back between each instruction.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.contentblocking.report.hide_vpn_banner", false]],
+ });
+
+ await BrowserTestUtils.reloadTab(tab);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const bannerExitLink = await ContentTaskUtils.waitForCondition(() => {
+ return content.document.querySelector(".vpn-banner .exit-icon");
+ }, "vpn banner exit link exists");
+ await ContentTaskUtils.waitForCondition(
+ () => ContentTaskUtils.isVisible(bannerExitLink),
+ "vpn banner exit link is visible"
+ );
+ EventUtils.sendMouseEvent(
+ { type: "click", button: 1 },
+ bannerExitLink,
+ content
+ );
+ });
+
+ events = await waitForTelemetryEventCount(7);
+ events = events.filter(
+ e =>
+ e[1] == "security.ui.protections" &&
+ e[2] == "click" &&
+ e[3] == "vpn_banner_close"
+ );
+ is(events.length, 1, `recorded telemetry for vpn_banner_close`);
+
+ // Clean up.
+ await BrowserTestUtils.removeTab(tab);
+}).skip();
diff --git a/browser/components/protections/test/browser/browser_protections_vpn.js b/browser/components/protections/test/browser/browser_protections_vpn.js
new file mode 100644
index 0000000000..2b54982ba9
--- /dev/null
+++ b/browser/components/protections/test/browser/browser_protections_vpn.js
@@ -0,0 +1,282 @@
+/* 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 { AboutProtectionsParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutProtectionsParent.sys.mjs"
+);
+
+let { Region } = ChromeUtils.importESModule(
+ "resource://gre/modules/Region.sys.mjs"
+);
+
+const initialHomeRegion = Region._home;
+const initialCurrentRegion = Region._current;
+
+async function checkVPNCardVisibility(tab, shouldBeHidden, subscribed = false) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ _shouldBeHidden: shouldBeHidden, _subscribed: subscribed }],
+ async function ({ _shouldBeHidden, _subscribed }) {
+ await ContentTaskUtils.waitForCondition(() => {
+ const vpnCard = content.document.querySelector(".vpn-card");
+ const subscribedStateCorrect =
+ vpnCard.classList.contains("subscribed") == _subscribed;
+ return (
+ ContentTaskUtils.isHidden(vpnCard) === _shouldBeHidden &&
+ subscribedStateCorrect
+ );
+ });
+
+ const visibilityState = _shouldBeHidden ? "hidden" : "shown";
+ ok(true, `VPN card is ${visibilityState}.`);
+ }
+ );
+}
+
+async function checkVPNPromoBannerVisibility(tab, shouldBeHidden) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ _shouldBeHidden: shouldBeHidden }],
+ async function ({ _shouldBeHidden }) {
+ await ContentTaskUtils.waitForCondition(() => {
+ const vpnBanner = content.document.querySelector(".vpn-banner");
+ return ContentTaskUtils.isHidden(vpnBanner) === _shouldBeHidden;
+ });
+
+ const visibilityState = _shouldBeHidden ? "hidden" : "shown";
+ ok(true, `VPN banner is ${visibilityState}.`);
+ }
+ );
+}
+
+async function setCurrentRegion(region) {
+ Region._setCurrentRegion(region);
+}
+
+async function setHomeRegion(region) {
+ // _setHomeRegion sets a char pref to the value of region. A non-string value will result in an error, so default to an empty string when region is falsey.
+ Region._setHomeRegion(region || "");
+}
+
+async function revertRegions() {
+ setCurrentRegion(initialCurrentRegion);
+ setHomeRegion(initialHomeRegion);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.vpn_promo.enabled", true],
+ ],
+ });
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ setCurrentRegion("us");
+ const avLocales = Services.locale.availableLocales;
+
+ registerCleanupFunction(() => {
+ Services.locale.availableLocales = avLocales;
+ });
+});
+
+add_task(async function testVPNCardVisibility() {
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ await promiseSetHomeRegion("us");
+ setCurrentRegion("us");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ info("Enable showing the VPN card");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"],
+ [
+ "browser.vpn_promo.disallowed_regions",
+ "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
+ ],
+ ],
+ });
+
+ info(
+ "Check that vpn card is hidden if neither the user's home nor current location is on the regions list."
+ );
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ setCurrentRegion("ls");
+ await promiseSetHomeRegion("ls");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNCardVisibility(tab, true);
+
+ info(
+ "Check that vpn card is hidden if user's location is in the list of disallowed regions."
+ );
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ setCurrentRegion("sy");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNCardVisibility(tab, true);
+
+ info(
+ "Check that vpn card shows a different version if user has subscribed to Mozilla vpn."
+ );
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(true));
+ setCurrentRegion("us");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNCardVisibility(tab, false, true);
+
+ info(
+ "VPN card should be hidden when vpn not enabled, though all other conditions are true"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.vpn_promo.enabled", false]],
+ });
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNCardVisibility(tab, true);
+
+ await BrowserTestUtils.removeTab(tab);
+ revertRegions();
+});
+
+add_task(async function testVPNPromoBanner() {
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ info("Enable showing the VPN card and banner");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"],
+ [
+ "browser.vpn_promo.disallowed_regions",
+ "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
+ ],
+ ["browser.contentblocking.report.hide_vpn_banner", false],
+ ],
+ });
+
+ info("Check that vpn banner is shown if user's region is supported");
+ setCurrentRegion("us");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, false);
+
+ is(
+ Services.prefs.getBoolPref(
+ "browser.contentblocking.report.hide_vpn_banner",
+ false
+ ),
+ true,
+ "After showing the banner once, the pref to hide the VPN banner is flipped"
+ );
+ info("The banner does not show when the pref to hide it is flipped");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+
+ // VPN Banner flips this pref each time it shows, flip back between each instruction.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.contentblocking.report.hide_vpn_banner", false]],
+ });
+
+ info(
+ "Check that VPN banner is hidden if user's location is not on the regions list."
+ );
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ setCurrentRegion("ls");
+ await setHomeRegion("ls'");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+
+ info(
+ "Check that VPN banner is hidden if user's location is in the disallowed regions list."
+ );
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ setCurrentRegion("sy");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+
+ info(
+ "VPN banner should be hidden when vpn not enabled, though all other conditions are true"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", false],
+ ["browser.contentblocking.report.hide_vpn_banner", false],
+ ],
+ });
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ ["browser.contentblocking.report.hide_vpn_banner", false],
+ ],
+ });
+
+ info("If user is subscribed to VPN already the promo banner should not show");
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(true));
+ setCurrentRegion("us");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+
+ await BrowserTestUtils.removeTab(tab);
+ revertRegions();
+});
+
+// Expect the vpn card and banner to not show as we are expressly excluding China. Even when cn is in the supported region pref.
+add_task(async function testVPNDoesNotShowChina() {
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ setCurrentRegion("us");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:protections",
+ gBrowser,
+ });
+
+ info("Enable showing the VPN card and banners");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.vpn_promo.enabled", true],
+ ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb,cn"],
+ [
+ "browser.vpn_promo.disallowed_regions",
+ "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr",
+ ],
+ ["browser.contentblocking.report.hide_vpn_banner", false],
+ ],
+ });
+
+ info(
+ "set home location to China, even though user is currently in the US, expect vpn card to be hidden"
+ );
+ await promiseSetHomeRegion("CN");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNCardVisibility(tab, true);
+
+ // VPN Banner flips this pref each time it shows, flip back between each instruction.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.contentblocking.report.hide_vpn_banner", false]],
+ });
+
+ info("home region is US, but current location is China");
+ AboutProtectionsParent.setTestOverride(getVPNOverrides(false));
+ await promiseSetHomeRegion("US");
+ setCurrentRegion("CN");
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNPromoBannerVisibility(tab, true);
+ await BrowserTestUtils.reloadTab(tab);
+ await checkVPNCardVisibility(tab, true);
+
+ await BrowserTestUtils.removeTab(tab);
+ revertRegions();
+});
diff --git a/browser/components/protections/test/browser/head.js b/browser/components/protections/test/browser/head.js
new file mode 100644
index 0000000000..9815869ee5
--- /dev/null
+++ b/browser/components/protections/test/browser/head.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+/* eslint-disable no-unused-vars */
+
+"use strict";
+
+const nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Region: "resource://gre/modules/Region.sys.mjs",
+});
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+const TEST_LOGIN1 = new nsLoginInfo(
+ "https://example.com/",
+ "https://example.com/",
+ null,
+ "user1",
+ "pass1",
+ "username",
+ "password"
+);
+
+const TEST_LOGIN2 = new nsLoginInfo(
+ "https://2.example.com/",
+ "https://2.example.com/",
+ null,
+ "user2",
+ "pass2",
+ "username",
+ "password"
+);
+
+// Used to replace AboutProtectionsHandler.getLoginData in front-end tests.
+const mockGetLoginDataWithSyncedDevices = (
+ mobileDeviceConnected = false,
+ potentiallyBreachedLogins = 0
+) => {
+ return {
+ getLoginData: () => {
+ return {
+ numLogins: Services.logins.countLogins("", "", ""),
+ potentiallyBreachedLogins,
+ mobileDeviceConnected,
+ };
+ },
+ };
+};
+
+// Used to replace AboutProtectionsHandler.getMonitorData in front-end tests.
+const mockGetMonitorData = data => {
+ return {
+ getMonitorData: () => {
+ if (data.error) {
+ return data;
+ }
+
+ return {
+ monitoredEmails: 1,
+ numBreaches: data.numBreaches,
+ passwords: 8,
+ numBreachesResolved: data.numBreachesResolved,
+ passwordsResolved: 1,
+ error: false,
+ };
+ },
+ };
+};
+
+registerCleanupFunction(function head_cleanup() {
+ Services.logins.removeAllUserFacingLogins();
+});
+
+// Used to replace AboutProtectionsParent.VPNSubStatus
+const getVPNOverrides = (hasSubscription = false) => {
+ return {
+ vpnOverrides: () => {
+ return hasSubscription;
+ },
+ };
+};
+
+const promiseSetHomeRegion = async region => {
+ let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Region._setHomeRegion(region);
+ await promise;
+};