From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../protections/content/lockwise-card.mjs | 142 +++ .../protections/content/monitor-card.mjs | 449 ++++++++ .../components/protections/content/protections.css | 1127 ++++++++++++++++++++ .../components/protections/content/protections.ftl | 26 + .../protections/content/protections.html | 597 +++++++++++ .../components/protections/content/protections.mjs | 490 +++++++++ .../components/protections/content/proxy-card.mjs | 29 + .../components/protections/content/vpn-card.mjs | 103 ++ 8 files changed, 2963 insertions(+) create mode 100644 browser/components/protections/content/lockwise-card.mjs create mode 100644 browser/components/protections/content/monitor-card.mjs create mode 100644 browser/components/protections/content/protections.css create mode 100644 browser/components/protections/content/protections.ftl create mode 100644 browser/components/protections/content/protections.html create mode 100644 browser/components/protections/content/protections.mjs create mode 100644 browser/components/protections/content/proxy-card.mjs create mode 100644 browser/components/protections/content/vpn-card.mjs (limited to 'browser/components/protections/content') 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
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
the Google Play Store or Apple App Store. + +vpn-banner-header = Protection that extends beyond the browser +# Note This text is not being translated, and the
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,
β€œ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 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+

+

+

+
+
+ + +
+
+

+

+
+ +
+
+
+
+

+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+

+

+ +

+
+ +
+

+

+ +

+
+
+

+

+ +

+
+
+

+

+ +

+
+
+
+
+
+
+ +
+ + + + + + + + +
+ + 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); + } +} -- cgit v1.2.3