tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

aboutPrivateBrowsing.js (10956B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Determines whether a given value is a fluent id or plain text and adds it to an element
      7 *
      8 * @param {Array<[HTMLElement, string]>} items An array of [element, value] where value is
      9 *                                       a fluent id starting with "fluent:" or plain text
     10 */
     11 function translateElements(items) {
     12  items.forEach(([element, value]) => {
     13    // Skip empty text or elements
     14    if (!element || !value) {
     15      return;
     16    }
     17    const fluentId = value.replace(/^fluent:/, "");
     18    if (fluentId !== value) {
     19      document.l10n.setAttributes(element, fluentId);
     20    } else {
     21      element.textContent = value;
     22      element.removeAttribute("data-l10n-id");
     23    }
     24  });
     25 }
     26 
     27 function renderInfo({
     28  infoEnabled,
     29  infoTitle,
     30  infoTitleEnabled,
     31  infoBody,
     32  infoLinkText,
     33  infoLinkUrl,
     34  infoIcon,
     35 } = {}) {
     36  const container = document.querySelector(".info");
     37  if (infoEnabled === false) {
     38    container.hidden = true;
     39    return;
     40  }
     41  container.hidden = false;
     42 
     43  const titleEl = document.getElementById("info-title");
     44  const bodyEl = document.getElementById("info-body");
     45  const linkEl = document.getElementById("private-browsing-myths");
     46 
     47  let feltPrivacyEnabled = RPMGetBoolPref(
     48    "browser.privatebrowsing.felt-privacy-v1",
     49    false
     50  );
     51 
     52  if (infoIcon && !feltPrivacyEnabled) {
     53    container.style.backgroundImage = `url(${infoIcon})`;
     54  }
     55 
     56  if (feltPrivacyEnabled) {
     57    // Record exposure event for Felt Privacy experiment
     58    window.FeltPrivacyExposureTelemetry();
     59 
     60    infoTitleEnabled = true;
     61    infoTitle = "fluent:about-private-browsing-felt-privacy-v1-info-header";
     62    infoBody = "fluent:about-private-browsing-felt-privacy-v1-info-body";
     63    infoLinkText = "fluent:about-private-browsing-felt-privacy-v1-info-link";
     64  }
     65 
     66  titleEl.hidden = !infoTitleEnabled;
     67 
     68  translateElements([
     69    [titleEl, infoTitle],
     70    [bodyEl, infoBody],
     71    [linkEl, infoLinkText],
     72  ]);
     73 
     74  if (infoLinkUrl) {
     75    linkEl.setAttribute("href", infoLinkUrl);
     76  }
     77 }
     78 
     79 async function renderPromo({
     80  messageId = null,
     81  promoEnabled = false,
     82  promoType = "VPN",
     83  promoTitle,
     84  promoTitleEnabled,
     85  promoLinkText,
     86  promoLinkType,
     87  promoSectionStyle,
     88  promoHeader,
     89  promoImageLarge,
     90  promoImageSmall,
     91  promoButton = null,
     92 } = {}) {
     93  const shouldShow = await RPMSendQuery("ShouldShowPromo", { type: promoType });
     94  const container = document.querySelector(".promo");
     95 
     96  if (!promoEnabled || !shouldShow) {
     97    container.remove();
     98    return false;
     99  }
    100 
    101  const titleEl = document.getElementById("private-browsing-promo-text");
    102  const linkEl = document.getElementById("private-browsing-promo-link");
    103  const promoHeaderEl = document.getElementById("promo-header");
    104  const infoContainerEl = document.querySelector(".info");
    105  const promoImageLargeEl = document.querySelector(".promo-image-large img");
    106  const promoImageSmallEl = document.querySelector(".promo-image-small img");
    107  const dismissBtn = document.querySelector("#dismiss-btn");
    108 
    109  if (promoLinkType === "link") {
    110    linkEl.classList.remove("primary");
    111    linkEl.classList.add("text-link", "promo-link");
    112  }
    113 
    114  if (promoButton?.action) {
    115    linkEl.addEventListener("click", async event => {
    116      event.preventDefault();
    117 
    118      // Record promo click telemetry and set metrics as allow for spotlight
    119      // modal opened on promo click if user is enrolled in an experiment
    120      let isExperiment = window.PrivateBrowsingRecordClick("PromoLink");
    121      const promoButtonData = promoButton?.action?.data;
    122      if (
    123        promoButton?.action?.type === "SHOW_SPOTLIGHT" &&
    124        promoButtonData?.content
    125      ) {
    126        promoButtonData.content.metrics = isExperiment ? "allow" : "block";
    127      }
    128 
    129      await RPMSendQuery("SpecialMessageActionDispatch", promoButton.action);
    130    });
    131  } else {
    132    // If the action doesn't exist, remove the promo completely
    133    container.remove();
    134    return false;
    135  }
    136 
    137  const onDismissBtnClick = () => {
    138    window.ASRouterMessage({
    139      type: "BLOCK_MESSAGE_BY_ID",
    140      data: { id: messageId },
    141    });
    142    window.PrivateBrowsingRecordClick("DismissButton");
    143    container.remove();
    144  };
    145 
    146  if (dismissBtn && messageId) {
    147    dismissBtn.addEventListener("click", onDismissBtnClick, { once: true });
    148  }
    149 
    150  if (promoSectionStyle) {
    151    container.classList.add(promoSectionStyle);
    152 
    153    switch (promoSectionStyle) {
    154      case "below-search":
    155        container.remove();
    156        infoContainerEl?.insertAdjacentElement("beforebegin", container);
    157        break;
    158      case "top":
    159        container.remove();
    160        document.body.insertAdjacentElement("afterbegin", container);
    161    }
    162  }
    163 
    164  if (promoImageLarge) {
    165    promoImageLargeEl.src = promoImageLarge;
    166  } else {
    167    promoImageLargeEl.parentNode.remove();
    168  }
    169 
    170  if (promoImageSmall) {
    171    promoImageSmallEl.src = promoImageSmall;
    172  } else {
    173    promoImageSmallEl.parentNode.remove();
    174  }
    175 
    176  if (!promoTitleEnabled) {
    177    titleEl.remove();
    178  }
    179 
    180  if (!promoHeader) {
    181    promoHeaderEl.remove();
    182  }
    183 
    184  translateElements([
    185    [titleEl, promoTitle],
    186    [linkEl, promoLinkText],
    187    [promoHeaderEl, promoHeader],
    188  ]);
    189 
    190  // Only make promo section visible after adding content
    191  // and translations to prevent layout shifting in page
    192  container.classList.add("promo-visible");
    193  return true;
    194 }
    195 
    196 /**
    197 * For every PB newtab loaded, a second is pre-rendered in the background.
    198 * We need to guard against invalid impressions by checking visibility state.
    199 * If visible, record. Otherwise, listen for visibility change and record later.
    200 */
    201 function recordOnceVisible(message) {
    202  const recordImpression = () => {
    203    if (document.visibilityState === "visible") {
    204      window.ASRouterMessage({
    205        type: "IMPRESSION",
    206        data: message,
    207      });
    208      // Similar telemetry, but for Nimbus experiments
    209      window.PrivateBrowsingPromoExposureTelemetry();
    210      document.removeEventListener("visibilitychange", recordImpression);
    211    }
    212  };
    213 
    214  if (document.visibilityState === "visible") {
    215    window.ASRouterMessage({
    216      type: "IMPRESSION",
    217      data: message,
    218    });
    219    // Similar telemetry, but for Nimbus experiments
    220    window.PrivateBrowsingPromoExposureTelemetry();
    221  } else {
    222    document.addEventListener("visibilitychange", recordImpression);
    223  }
    224 }
    225 
    226 // 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.
    227 function handlePromoOnPreload(message) {
    228  async function removePromoIfBlocked() {
    229    if (document.visibilityState === "visible") {
    230      let blocked = await RPMSendQuery("IsPromoBlocked", message);
    231      if (blocked) {
    232        const container = document.querySelector(".promo");
    233        container.remove();
    234      }
    235    }
    236    document.removeEventListener("visibilitychange", removePromoIfBlocked);
    237  }
    238  // Only add the listener to pre-rendered tabs that aren't visible
    239  if (document.visibilityState !== "visible") {
    240    document.addEventListener("visibilitychange", removePromoIfBlocked);
    241  }
    242 }
    243 
    244 async function setupMessageConfig(config = null) {
    245  let message = null;
    246 
    247  if (!config) {
    248    let hideDefault = window.PrivateBrowsingShouldHideDefault();
    249    try {
    250      let response = await window.ASRouterMessage({
    251        type: "PBNEWTAB_MESSAGE_REQUEST",
    252        data: { hideDefault: !!hideDefault },
    253      });
    254      message = response?.message;
    255      config = message?.content;
    256      config.messageId = message?.id;
    257    } catch (e) {}
    258  }
    259 
    260  renderInfo(config);
    261  let hasRendered = await renderPromo(config);
    262  if (hasRendered && message) {
    263    recordOnceVisible(message);
    264    handlePromoOnPreload(message);
    265  }
    266  // For tests
    267  document.documentElement.setAttribute("PrivateBrowsingRenderComplete", true);
    268 }
    269 
    270 let SHOW_DEVTOOLS_MESSAGE = "ShowDevToolsMessage";
    271 
    272 function showDevToolsMessage(msg) {
    273  msg.data.content.messageId = "DEVTOOLS_MESSAGE";
    274  setupMessageConfig(msg?.data?.content);
    275  RPMRemoveMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage);
    276 }
    277 
    278 document.addEventListener("DOMContentLoaded", function () {
    279  // check the url to see if we're rendering a devtools message
    280  if (document.location.toString().includes("debug")) {
    281    RPMAddMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage);
    282    return;
    283  }
    284  if (!RPMIsWindowPrivate()) {
    285    document.documentElement.classList.remove("private");
    286    document.documentElement.classList.add("normal");
    287    document
    288      .getElementById("startPrivateBrowsing")
    289      .addEventListener("click", function () {
    290        RPMSendAsyncMessage("OpenPrivateWindow");
    291      });
    292    return;
    293  }
    294 
    295  // The default info content is already in the markup, but we need to use JS to
    296  // set up the learn more link, since it's dynamically generated.
    297  const linkEl = document.getElementById("private-browsing-myths");
    298  linkEl.setAttribute(
    299    "href",
    300    RPMGetFormatURLPref("app.support.baseURL") + "private-browsing-myths"
    301  );
    302  linkEl.addEventListener("click", () => {
    303    window.PrivateBrowsingRecordClick("InfoLink");
    304  });
    305 
    306  // We don't do this setup until now, because we don't want to record any impressions until we're
    307  // sure we're actually running a private window, not just about:privatebrowsing in a normal window.
    308  setupMessageConfig();
    309 
    310  // Set up the private search banner.
    311  const privateSearchBanner = document.getElementById("search-banner");
    312 
    313  RPMSendQuery("ShouldShowSearchBanner", {}).then(engineName => {
    314    if (engineName) {
    315      document.l10n.setAttributes(
    316        document.getElementById("about-private-browsing-search-banner-title"),
    317        "about-private-browsing-search-banner-title",
    318        { engineName }
    319      );
    320      privateSearchBanner.removeAttribute("hidden");
    321      document.body.classList.add("showBanner");
    322    }
    323 
    324    // We set this attribute so that tests know when we are done.
    325    document.documentElement.setAttribute("SearchBannerInitialized", true);
    326  });
    327 
    328  function hideSearchBanner() {
    329    privateSearchBanner.hidden = true;
    330    document.body.classList.remove("showBanner");
    331    RPMSendAsyncMessage("SearchBannerDismissed");
    332  }
    333 
    334  document
    335    .getElementById("search-banner-close-button")
    336    .addEventListener("click", () => {
    337      hideSearchBanner();
    338    });
    339 
    340  let openSearchOptions = document.getElementById(
    341    "about-private-browsing-search-banner-description"
    342  );
    343  let openSearchOptionsEvtHandler = evt => {
    344    if (
    345      evt.target.id == "open-search-options-link" &&
    346      (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click")
    347    ) {
    348      RPMSendAsyncMessage("OpenSearchPreferences");
    349      hideSearchBanner();
    350    }
    351  };
    352  openSearchOptions.addEventListener("click", openSearchOptionsEvtHandler);
    353  openSearchOptions.addEventListener("keypress", openSearchOptionsEvtHandler);
    354 });