diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/protections | |
parent | Initial commit. (diff) | |
download | firefox-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')
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; +}; |