forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			435 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			435 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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) {
 | 
						|
    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.PrivateBrowsingExposureTelemetry();
 | 
						|
      document.removeEventListener("visibilitychange", recordImpression);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  if (document.visibilityState === "visible") {
 | 
						|
    window.ASRouterMessage({
 | 
						|
      type: "IMPRESSION",
 | 
						|
      data: message,
 | 
						|
    });
 | 
						|
    // Similar telemetry, but for Nimbus experiments
 | 
						|
    window.PrivateBrowsingExposureTelemetry();
 | 
						|
  } 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");
 | 
						|
  RPMSendQuery("ShouldShowSearch", {}).then(
 | 
						|
    ([engineName, shouldHandOffToSearchMode]) => {
 | 
						|
      let input = document.querySelector(".fake-textbox");
 | 
						|
      if (shouldHandOffToSearchMode) {
 | 
						|
        document.l10n.setAttributes(btn, "about-private-browsing-search-btn");
 | 
						|
        document.l10n.setAttributes(
 | 
						|
          input,
 | 
						|
          "about-private-browsing-search-placeholder"
 | 
						|
        );
 | 
						|
      } else if (engineName) {
 | 
						|
        document.l10n.setAttributes(btn, "about-private-browsing-handoff", {
 | 
						|
          engine: engineName,
 | 
						|
        });
 | 
						|
        document.l10n.setAttributes(
 | 
						|
          input,
 | 
						|
          "about-private-browsing-handoff-text",
 | 
						|
          {
 | 
						|
            engine: engineName,
 | 
						|
          }
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        document.l10n.setAttributes(
 | 
						|
          btn,
 | 
						|
          "about-private-browsing-handoff-no-engine"
 | 
						|
        );
 | 
						|
        document.l10n.setAttributes(
 | 
						|
          input,
 | 
						|
          "about-private-browsing-handoff-text-no-engine"
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
  );
 | 
						|
 | 
						|
  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();
 | 
						|
});
 |