summaryrefslogtreecommitdiffstats
path: root/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/privatebrowsing/content/aboutPrivateBrowsing.js')
-rw-r--r--browser/components/privatebrowsing/content/aboutPrivateBrowsing.js406
1 files changed, 406 insertions, 0 deletions
diff --git a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
new file mode 100644
index 0000000000..0f0e23d81c
--- /dev/null
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js
@@ -0,0 +1,406 @@
+/* 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 */
+
+/**
+ * Determines whether a given value is a fluent id or plain text and adds it to an element
+ * @param {Array<[HTMLElement, string]>} items An array of [element, value] where value is
+ * a fluent id starting with "fluent:" or plain text
+ */
+function translateElements(items) {
+ items.forEach(([element, value]) => {
+ // Skip empty text or elements
+ if (!element || !value) {
+ return;
+ }
+ const fluentId = value.replace(/^fluent:/, "");
+ if (fluentId !== value) {
+ document.l10n.setAttributes(element, fluentId);
+ } else {
+ element.textContent = value;
+ element.removeAttribute("data-l10n-id");
+ }
+ });
+}
+
+function renderInfo({
+ infoEnabled,
+ infoTitle,
+ infoTitleEnabled,
+ infoBody,
+ infoLinkText,
+ infoLinkUrl,
+ infoIcon,
+} = {}) {
+ const container = document.querySelector(".info");
+ if (infoEnabled === false) {
+ container.hidden = true;
+ return;
+ }
+ container.hidden = false;
+
+ const titleEl = document.getElementById("info-title");
+ const bodyEl = document.getElementById("info-body");
+ const linkEl = document.getElementById("private-browsing-myths");
+
+ let feltPrivacyEnabled = RPMGetBoolPref(
+ "browser.privatebrowsing.felt-privacy-v1",
+ false
+ );
+
+ if (infoIcon && !feltPrivacyEnabled) {
+ container.style.backgroundImage = `url(${infoIcon})`;
+ }
+
+ if (feltPrivacyEnabled) {
+ // Record exposure event for Felt Privacy experiment
+ window.FeltPrivacyExposureTelemetry();
+
+ infoTitleEnabled = true;
+ infoTitle = "fluent:about-private-browsing-felt-privacy-v1-info-header";
+ infoBody = "fluent:about-private-browsing-felt-privacy-v1-info-body";
+ infoLinkText = "fluent:about-private-browsing-felt-privacy-v1-info-link";
+ }
+
+ titleEl.hidden = !infoTitleEnabled;
+
+ translateElements([
+ [titleEl, infoTitle],
+ [bodyEl, infoBody],
+ [linkEl, infoLinkText],
+ ]);
+
+ if (infoLinkUrl) {
+ linkEl.setAttribute("href", infoLinkUrl);
+ }
+}
+
+async function renderPromo({
+ messageId = null,
+ promoEnabled = false,
+ promoType = "VPN",
+ promoTitle,
+ promoTitleEnabled,
+ promoLinkText,
+ promoLinkType,
+ promoSectionStyle,
+ promoHeader,
+ promoImageLarge,
+ promoImageSmall,
+ promoButton = null,
+} = {}) {
+ const shouldShow = await RPMSendQuery("ShouldShowPromo", { type: promoType });
+ const container = document.querySelector(".promo");
+
+ if (!promoEnabled || !shouldShow) {
+ container.remove();
+ return false;
+ }
+
+ const titleEl = document.getElementById("private-browsing-promo-text");
+ const linkEl = document.getElementById("private-browsing-promo-link");
+ const promoHeaderEl = document.getElementById("promo-header");
+ const infoContainerEl = document.querySelector(".info");
+ const promoImageLargeEl = document.querySelector(".promo-image-large img");
+ const promoImageSmallEl = document.querySelector(".promo-image-small img");
+ const dismissBtn = document.querySelector("#dismiss-btn");
+
+ if (promoLinkType === "link") {
+ linkEl.classList.remove("primary");
+ linkEl.classList.add("text-link", "promo-link");
+ }
+
+ if (promoButton?.action) {
+ linkEl.addEventListener("click", async event => {
+ event.preventDefault();
+
+ // Record promo click telemetry and set metrics as allow for spotlight
+ // modal opened on promo click if user is enrolled in an experiment
+ let isExperiment = window.PrivateBrowsingRecordClick("promo_link");
+ const promoButtonData = promoButton?.action?.data;
+ if (
+ promoButton?.action?.type === "SHOW_SPOTLIGHT" &&
+ promoButtonData?.content
+ ) {
+ promoButtonData.content.metrics = isExperiment ? "allow" : "block";
+ }
+
+ await RPMSendQuery("SpecialMessageActionDispatch", promoButton.action);
+ });
+ } else {
+ // If the action doesn't exist, remove the promo completely
+ container.remove();
+ return false;
+ }
+
+ const onDismissBtnClick = () => {
+ window.ASRouterMessage({
+ type: "BLOCK_MESSAGE_BY_ID",
+ data: { id: messageId },
+ });
+ window.PrivateBrowsingRecordClick("dismiss_button");
+ container.remove();
+ };
+
+ if (dismissBtn && messageId) {
+ dismissBtn.addEventListener("click", onDismissBtnClick, { once: true });
+ }
+
+ if (promoSectionStyle) {
+ container.classList.add(promoSectionStyle);
+
+ switch (promoSectionStyle) {
+ case "below-search":
+ container.remove();
+ infoContainerEl?.insertAdjacentElement("beforebegin", container);
+ break;
+ case "top":
+ container.remove();
+ document.body.insertAdjacentElement("afterbegin", container);
+ }
+ }
+
+ if (promoImageLarge) {
+ promoImageLargeEl.src = promoImageLarge;
+ } else {
+ promoImageLargeEl.parentNode.remove();
+ }
+
+ if (promoImageSmall) {
+ promoImageSmallEl.src = promoImageSmall;
+ } else {
+ promoImageSmallEl.parentNode.remove();
+ }
+
+ if (!promoTitleEnabled) {
+ titleEl.remove();
+ }
+
+ if (!promoHeader) {
+ promoHeaderEl.remove();
+ }
+
+ translateElements([
+ [titleEl, promoTitle],
+ [linkEl, promoLinkText],
+ [promoHeaderEl, promoHeader],
+ ]);
+
+ // Only make promo section visible after adding content
+ // and translations to prevent layout shifting in page
+ container.classList.add("promo-visible");
+ return true;
+}
+
+/**
+ * For every PB newtab loaded, a second is pre-rendered in the background.
+ * We need to guard against invalid impressions by checking visibility state.
+ * If visible, record. Otherwise, listen for visibility change and record later.
+ */
+function recordOnceVisible(message) {
+ const recordImpression = () => {
+ if (document.visibilityState === "visible") {
+ window.ASRouterMessage({
+ type: "IMPRESSION",
+ data: message,
+ });
+ // Similar telemetry, but for Nimbus experiments
+ window.PrivateBrowsingPromoExposureTelemetry();
+ document.removeEventListener("visibilitychange", recordImpression);
+ }
+ };
+
+ if (document.visibilityState === "visible") {
+ window.ASRouterMessage({
+ type: "IMPRESSION",
+ data: message,
+ });
+ // Similar telemetry, but for Nimbus experiments
+ window.PrivateBrowsingPromoExposureTelemetry();
+ } else {
+ document.addEventListener("visibilitychange", recordImpression);
+ }
+}
+
+// The PB newtab may be pre-rendered. Once the tab is visible, check to make sure the message wasn't blocked after the initial render. If it was, remove the promo.
+function handlePromoOnPreload(message) {
+ async function removePromoIfBlocked() {
+ if (document.visibilityState === "visible") {
+ let blocked = await RPMSendQuery("IsPromoBlocked", message);
+ if (blocked) {
+ const container = document.querySelector(".promo");
+ container.remove();
+ }
+ }
+ document.removeEventListener("visibilitychange", removePromoIfBlocked);
+ }
+ // Only add the listener to pre-rendered tabs that aren't visible
+ if (document.visibilityState !== "visible") {
+ document.addEventListener("visibilitychange", removePromoIfBlocked);
+ }
+}
+
+async function setupMessageConfig(config = null) {
+ let message = null;
+
+ if (!config) {
+ let hideDefault = window.PrivateBrowsingShouldHideDefault();
+ try {
+ let response = await window.ASRouterMessage({
+ type: "PBNEWTAB_MESSAGE_REQUEST",
+ data: { hideDefault: !!hideDefault },
+ });
+ message = response?.message;
+ config = message?.content;
+ config.messageId = message?.id;
+ } catch (e) {}
+ }
+
+ renderInfo(config);
+ let hasRendered = await renderPromo(config);
+ if (hasRendered && message) {
+ recordOnceVisible(message);
+ handlePromoOnPreload(message);
+ }
+ // For tests
+ document.documentElement.setAttribute("PrivateBrowsingRenderComplete", true);
+}
+
+let SHOW_DEVTOOLS_MESSAGE = "ShowDevToolsMessage";
+
+function showDevToolsMessage(msg) {
+ msg.data.content.messageId = "DEVTOOLS_MESSAGE";
+ setupMessageConfig(msg?.data?.content);
+ RPMRemoveMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage);
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+ // check the url to see if we're rendering a devtools message
+ if (document.location.toString().includes("debug")) {
+ RPMAddMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage);
+ return;
+ }
+ if (!RPMIsWindowPrivate()) {
+ document.documentElement.classList.remove("private");
+ document.documentElement.classList.add("normal");
+ document
+ .getElementById("startPrivateBrowsing")
+ .addEventListener("click", function () {
+ RPMSendAsyncMessage("OpenPrivateWindow");
+ });
+ return;
+ }
+
+ // The default info content is already in the markup, but we need to use JS to
+ // set up the learn more link, since it's dynamically generated.
+ const linkEl = document.getElementById("private-browsing-myths");
+ linkEl.setAttribute(
+ "href",
+ RPMGetFormatURLPref("app.support.baseURL") + "private-browsing-myths"
+ );
+ linkEl.addEventListener("click", () => {
+ window.PrivateBrowsingRecordClick("info_link");
+ });
+
+ // We don't do this setup until now, because we don't want to record any impressions until we're
+ // sure we're actually running a private window, not just about:privatebrowsing in a normal window.
+ setupMessageConfig();
+
+ // Set up the private search banner.
+ const privateSearchBanner = document.getElementById("search-banner");
+
+ RPMSendQuery("ShouldShowSearchBanner", {}).then(engineName => {
+ if (engineName) {
+ document.l10n.setAttributes(
+ document.getElementById("about-private-browsing-search-banner-title"),
+ "about-private-browsing-search-banner-title",
+ { engineName }
+ );
+ privateSearchBanner.removeAttribute("hidden");
+ document.body.classList.add("showBanner");
+ }
+
+ // We set this attribute so that tests know when we are done.
+ document.documentElement.setAttribute("SearchBannerInitialized", true);
+ });
+
+ function hideSearchBanner() {
+ privateSearchBanner.hidden = true;
+ document.body.classList.remove("showBanner");
+ RPMSendAsyncMessage("SearchBannerDismissed");
+ }
+
+ document
+ .getElementById("search-banner-close-button")
+ .addEventListener("click", () => {
+ hideSearchBanner();
+ });
+
+ let openSearchOptions = document.getElementById(
+ "about-private-browsing-search-banner-description"
+ );
+ let openSearchOptionsEvtHandler = evt => {
+ if (
+ evt.target.id == "open-search-options-link" &&
+ (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click")
+ ) {
+ RPMSendAsyncMessage("OpenSearchPreferences");
+ hideSearchBanner();
+ }
+ };
+ openSearchOptions.addEventListener("click", openSearchOptionsEvtHandler);
+ openSearchOptions.addEventListener("keypress", openSearchOptionsEvtHandler);
+
+ // Setup the search hand-off box.
+ let btn = document.getElementById("search-handoff-button");
+
+ let editable = document.getElementById("fake-editable");
+ let DISABLE_SEARCH_TOPIC = "DisableSearch";
+ let SHOW_SEARCH_TOPIC = "ShowSearch";
+ let SEARCH_HANDOFF_TOPIC = "SearchHandoff";
+
+ function showSearch() {
+ btn.classList.remove("focused");
+ btn.classList.remove("disabled");
+ RPMRemoveMessageListener(SHOW_SEARCH_TOPIC, showSearch);
+ }
+
+ function disableSearch() {
+ btn.classList.add("disabled");
+ }
+
+ function handoffSearch(text) {
+ RPMSendAsyncMessage(SEARCH_HANDOFF_TOPIC, { text });
+ RPMAddMessageListener(SHOW_SEARCH_TOPIC, showSearch);
+ if (text) {
+ disableSearch();
+ } else {
+ btn.classList.add("focused");
+ RPMAddMessageListener(DISABLE_SEARCH_TOPIC, disableSearch);
+ }
+ }
+ btn.addEventListener("focus", function () {
+ handoffSearch();
+ });
+ btn.addEventListener("click", function () {
+ handoffSearch();
+ });
+
+ // Hand-off any text that gets dropped or pasted
+ editable.addEventListener("drop", function (ev) {
+ ev.preventDefault();
+ let text = ev.dataTransfer.getData("text");
+ if (text) {
+ handoffSearch(text);
+ }
+ });
+ editable.addEventListener("paste", function (ev) {
+ ev.preventDefault();
+ handoffSearch(ev.clipboardData.getData("Text"));
+ });
+
+ // Load contentSearchUI so it sets the search engine icon and name for us.
+ new window.ContentSearchHandoffUIController();
+});