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 });