aboutTor.js (15230B)
1 "use strict"; 2 3 const SearchWidget = { 4 _initialized: false, 5 _initialOnionize: false, 6 7 /** 8 * Initialize the search form elements. 9 */ 10 init() { 11 this._initialized = true; 12 13 this.searchForm = document.getElementById("search-form"); 14 this.onionizeToggle = document.getElementById("onionize-toggle"); 15 this.onionizeToggle.pressed = this._initialOnionize; 16 this._updateOnionize(); 17 this.onionizeToggle.addEventListener("toggle", () => 18 this._updateOnionize() 19 ); 20 21 // If the user submits, save the onionize search state for the next about:tor 22 // page. 23 this.searchForm.addEventListener("submit", () => { 24 dispatchEvent( 25 new CustomEvent("SubmitSearchOnionize", { 26 detail: this.onionizeToggle.pressed, 27 bubbles: true, 28 }) 29 ); 30 }); 31 32 // By default, Enter on the onionizeToggle will toggle the button rather 33 // than submit the <form>. 34 // Moreover, our <form> has no submit button, so can only be submitted by 35 // pressing Enter. 36 // For keyboard users, Space will also toggle the form. We do not want to 37 // require users to have to Tab back to the search input in order to press 38 // Enter to submit the form. 39 // For mouse users, clicking the toggle button will give it focus, so they 40 // would have to Tab back or click the search input in order to submit the 41 // form. 42 // So we want to intercept the Enter keydown event to submit the form. 43 this.onionizeToggle.addEventListener( 44 "keydown", 45 event => { 46 if (event.key !== "Enter") { 47 return; 48 } 49 event.preventDefault(); 50 event.stopPropagation(); 51 this.searchForm.requestSubmit(); 52 }, 53 { capture: true } 54 ); 55 }, 56 57 _updateOnionize() { 58 // Change submit URL based on the onionize toggle. 59 this.searchForm.action = this.onionizeToggle.pressed 60 ? "https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion" 61 : "https://duckduckgo.com"; 62 this.searchForm.classList.toggle( 63 "onionized-search", 64 this.onionizeToggle.pressed 65 ); 66 }, 67 68 /** 69 * Set what the "Onionize" toggle state. 70 * 71 * @param {boolean} state - Whether the "Onionize" toggle should be switched 72 * on. 73 */ 74 setOnionizeState(state) { 75 if (!this._initialized) { 76 this._initialOnionize = state; 77 return; 78 } 79 this.onionizeToggle.pressed = state; 80 this._updateOnionize(); 81 }, 82 }; 83 84 const MessageArea = { 85 _initialized: false, 86 _messageData: null, 87 _isStable: null, 88 _torConnectEnabled: null, 89 90 /** 91 * Initialize the message area and heading once elements are available. 92 */ 93 init() { 94 this._initialized = true; 95 this._update(); 96 }, 97 98 /** 99 * Set the message data and stable release flag. 100 * 101 * @param {MessageData} messageData - The message data, indicating which 102 * message to show. 103 * @param {boolean} isStable - Whether this is the stable release version. 104 * @param {boolean} torConnectEnabled - Whether TorConnect is enabled, and 105 * therefore the Tor process was configured with about:torconnect. 106 */ 107 setMessageData(messageData, isStable, torConnectEnabled) { 108 this._messageData = messageData; 109 this._isStable = isStable; 110 this._torConnectEnabled = torConnectEnabled; 111 this._update(); 112 }, 113 114 _update() { 115 if (!this._initialized) { 116 return; 117 } 118 119 document 120 .querySelector(".home-message.shown-message") 121 ?.classList.remove("shown-message"); 122 123 if (!this._messageData) { 124 return; 125 } 126 127 // Set heading. 128 document.body.classList.toggle("is-testing", !this._isStable); 129 130 document.body.classList.toggle("show-tor-check", !this._torConnectEnabled); 131 132 const { updateVersion, updateURL, number } = this._messageData; 133 134 if (updateVersion) { 135 const updatedElement = document.getElementById("home-message-updated"); 136 updatedElement.querySelector("a").href = updateURL; 137 document.l10n.setAttributes( 138 updatedElement.querySelector("span"), 139 "tor-browser-home-message-updated", 140 { version: updateVersion } 141 ); 142 updatedElement.classList.add("shown-message"); 143 } else { 144 const messageElements = document.querySelectorAll( 145 this._isStable 146 ? ".home-message-rotating-stable" 147 : ".home-message-rotating-testing" 148 ); 149 messageElements[number % messageElements.length].classList.add( 150 "shown-message" 151 ); 152 } 153 }, 154 }; 155 156 /** 157 * A reusable area for surveys. 158 * 159 * Initially used for tor-browser#43504. 160 */ 161 const SurveyArea = { 162 /** 163 * The current version of the survey. 164 * 165 * Should be increased every time we start a new survey campaign. 166 * 167 * @type {integer} 168 */ 169 _version: 1, 170 171 /** 172 * The date to start showing the survey. 173 * 174 * @type {?integer} 175 */ 176 _startDate: null, // No survey date. 177 178 /** 179 * The date to stop showing the current survey. 180 * 181 * @type {?integer} 182 */ 183 _endDate: null, // No survey date. 184 185 /** 186 * The survey URL. 187 * 188 * @type {string} 189 */ 190 _urlBase: "https://survey.torproject.org/index.php/923269", 191 192 /** 193 * @typedef {object} SurveyLocaleData 194 * 195 * Locale-specific data for the survey. 196 * 197 * @property {string[]} browserLocales - The browser locales this should match 198 * with. The first locale should match the locale of the strings. 199 * @property {string} urlCode - The language code to pass to the survey URL. 200 * @property {string} dir - The direction of the locale. 201 * @property {object} strings - The strings to use for the survey banner. 202 */ 203 204 /** 205 * The data for the selected locale. 206 * 207 * @type {SurveyLocaleData} 208 */ 209 _localeData: null, 210 211 /** 212 * The data for each locale that is supported. 213 * 214 * The first entry is the default. 215 * 216 * @type {SurveyLocaleData[]} 217 */ 218 _localeDataSet: [ 219 { 220 browserLocales: ["en-US"], 221 dir: "ltr", 222 urlCode: "en", 223 strings: { 224 heading: "We’d love your feedback", 225 body: "Help us improve Tor Browser by completing this 10-minute survey.", 226 launch: "Launch the survey", 227 dismiss: "Dismiss", 228 close: "Close", 229 }, 230 }, 231 { 232 browserLocales: ["es-ES"], 233 dir: "ltr", 234 urlCode: "es", 235 strings: { 236 heading: "Danos tu opinión", 237 body: "Ayúdanos a mejorar el Navegador Tor completando esta encuesta de 10 minutos.", 238 launch: "Iniciar la encuesta", 239 dismiss: "Descartar", 240 close: "Cerrar", 241 }, 242 }, 243 { 244 browserLocales: ["ru"], 245 dir: "ltr", 246 urlCode: "ru", 247 strings: { 248 heading: "Мы будем рады вашим отзывам", 249 body: "Помогите нам улучшить браузер Tor, пройдя 10-минутный опрос.", 250 launch: "Начать опрос", 251 dismiss: "Отклонить", 252 close: "Закрыть", 253 }, 254 }, 255 { 256 browserLocales: ["fr"], 257 dir: "ltr", 258 urlCode: "fr", 259 strings: { 260 heading: "Nous serions ravis d’avoir votre avis !", 261 body: "Aidez-nous à améliorer le navigateur Tor en répondant à cette enquête de 10 minutes.", 262 launch: "Lancer l'enquête", 263 dismiss: "Ignorer", 264 close: "Fermer", 265 }, 266 }, 267 { 268 // Also show this pt-BR banner for the pt-PT browser locale. 269 browserLocales: ["pt-BR", "pt-PT"], 270 dir: "ltr", 271 urlCode: "pt-BR", 272 strings: { 273 heading: "Adoraríamos ouvir sua opinião", 274 body: "Ajude-nos a melhorar o Navegador Tor respondendo a esta pesquisa de 10 minutos.", 275 launch: "Iniciar a pesquisa", 276 dismiss: "Dispensar", 277 close: "Fechar", 278 }, 279 }, 280 ], 281 282 /** 283 * Whether the area has been initialised. 284 * 285 * @type {boolean} 286 */ 287 _initialized: false, 288 289 /** 290 * The app locale, or `null` whilst unset. 291 * 292 * @type {?string} 293 */ 294 _locale: null, 295 296 /** 297 * Whether the banner should be shown. 298 * 299 * @type {boolean} 300 */ 301 _shouldShow: false, 302 303 /** 304 * The survey element. 305 * 306 * @type {?Element} 307 */ 308 _areaEl: null, 309 310 /** 311 * Initialize the survey area. 312 */ 313 init() { 314 this._initialized = true; 315 316 this._areaEl = document.getElementById("survey"); 317 document.getElementById("survey-launch").addEventListener("click", () => { 318 const url = URL.parse(this._urlBase); 319 if (!url || !this._localeData) { 320 return; 321 } 322 323 url.searchParams.append("lang", this._localeData.urlCode); 324 open(url.href); 325 }); 326 document.getElementById("survey-close").addEventListener("click", () => { 327 this._hide(); 328 }); 329 document.getElementById("survey-dismiss").addEventListener("click", () => { 330 this._hide(); 331 }); 332 333 this._update(); 334 }, 335 336 /** 337 * Permanently hide this survey. 338 */ 339 _hide() { 340 this._shouldShow = false; 341 this._update(); 342 343 dispatchEvent( 344 new CustomEvent("SurveyDismissed", { 345 // We pass in the current survey version to record the *latest* 346 // version that the user has dismissed. This will overwrite any 347 // previous versions. 348 detail: this._version, 349 bubbles: true, 350 }) 351 ); 352 }, 353 354 /** 355 * Decide whether to show the survey. 356 * 357 * @param {integer} dismissVersion - The latest version of survey that the 358 * user has already dismissed. 359 * @param {boolean} isStable - Whether this is the stable release of Tor 360 * Browser. 361 * @param {string} appLocale - The app locale currently in use. 362 */ 363 potentiallyShow(dismissVersion, isStable, appLocale) { 364 const now = Date.now(); 365 this._shouldShow = 366 isStable && 367 dismissVersion < this._version && 368 this._startDate && 369 now >= this._startDate && 370 now < this._endDate; 371 this._locale = appLocale; 372 this._update(); 373 }, 374 375 /** 376 * Update the display. 377 */ 378 _update() { 379 if (!this._initialized) { 380 return; 381 } 382 if (!this._shouldShow) { 383 if (this._areaEl.contains(document.activeElement)) { 384 // Move focus to the search input. 385 document.getElementById("search-input").focus(); 386 } 387 document.body.classList.remove("show-survey"); 388 return; 389 } 390 391 // Determine the survey locale based on the app locale. 392 // NOTE: We do not user document.l10n to translate the survey banner. 393 // Instead we only translate the banner into a limited set of locales that 394 // match the languages that the survey itself supports. This should match 395 // the language of the survey when it is opened by the user. 396 for (const localeData of this._localeDataSet) { 397 if (localeData.browserLocales.includes(this._locale)) { 398 this._localeData = localeData; 399 break; 400 } 401 } 402 if (!this._localeData) { 403 // Show the default en-US banner. 404 this._localeData = this._localeDataSet[0]; 405 } 406 407 // Make sure the survey's lang and dir attributes match the chosen locale. 408 const surveyEl = document.getElementById("survey"); 409 surveyEl.setAttribute("lang", this._localeData.browserLocales[0]); 410 surveyEl.setAttribute("dir", this._localeData.dir); 411 412 const { heading, body, launch, dismiss, close } = this._localeData.strings; 413 414 document.getElementById("survey-heading").textContent = heading; 415 document.getElementById("survey-body").textContent = body; 416 document.getElementById("survey-launch").textContent = launch; 417 document.getElementById("survey-dismiss").textContent = dismiss; 418 document.getElementById("survey-close").setAttribute("title", close); 419 420 document.body.classList.add("show-survey"); 421 }, 422 }; 423 424 /** 425 * Area for the Year End Campaign (YEC). 426 * See tor-browser#42072. 427 */ 428 const YecArea = { 429 /** 430 * The epoch time to start showing the banner, if at all. 431 * 432 * @type {?integer} 433 */ 434 _startDate: null, // No YEC is active. 435 436 /** 437 * The epoch time to stop showing the banner, if at all. 438 * 439 * @type {?integer} 440 */ 441 _endDate: null, // No YEC is active. 442 443 /** 444 * Whether the area has been initialised. 445 * 446 * @type {boolean} 447 */ 448 _initialized: false, 449 450 /** 451 * The app locale, or `null` whilst unset. 452 * 453 * @type {?string} 454 */ 455 _locale: null, 456 457 /** 458 * Whether the banner should be shown. 459 * 460 * @type {boolean} 461 */ 462 _shouldShow: false, 463 464 /** 465 * The banner element. 466 * 467 * @type {?Element} 468 */ 469 _areaEl: null, 470 471 /** 472 * Initialize the widget. 473 */ 474 init() { 475 this._initialized = true; 476 477 this._areaEl = document.getElementById("yec-banner"); 478 479 document.getElementById("yec-close").addEventListener("click", () => { 480 this.dismiss(); 481 dispatchEvent(new CustomEvent("UserDismissedYEC", { bubbles: true })); 482 }); 483 484 this._update(); 485 }, 486 487 /** 488 * Close the banner. 489 */ 490 dismiss() { 491 this._shouldShow = false; 492 this._update(); 493 }, 494 495 /** 496 * Possibly show the banner. 497 * 498 * @param {boolean} dismissYEC - Whether the user has dismissed YEC. 499 * @param {boolean} isStable - Whether this is a stable release. 500 * @param {string} appLocale - The app locale, as BCP47. 501 */ 502 potentiallyShow(dismissYEC, isStable, appLocale) { 503 const now = Date.now(); 504 this._shouldShow = 505 !dismissYEC && 506 isStable && 507 this._startDate && 508 now >= this._startDate && 509 now < this._endDate; 510 this._locale = appLocale; 511 this._update(); 512 }, 513 514 /** 515 * Update the visibility of the banner to reflect the new state. 516 */ 517 _update() { 518 if (!this._initialized) { 519 return; 520 } 521 if (!this._shouldShow) { 522 if (this._areaEl.contains(document.activeElement)) { 523 document.documentElement.focus(); 524 } 525 document.body.classList.remove("show-yec"); 526 return; 527 } 528 529 const donateLink = document.getElementById("yec-donate-link"); 530 const base = "https://www.torproject.org/donate"; 531 donateLink.href = base; 532 533 document.body.classList.add("show-yec"); 534 }, 535 }; 536 537 let gInitialData = false; 538 let gLoaded = false; 539 540 function maybeComplete() { 541 if (!gInitialData || !gLoaded) { 542 return; 543 } 544 // Wait to show the content when the l10n population has completed. 545 if (document.hasPendingL10nMutations) { 546 window.addEventListener( 547 "L10nMutationsFinished", 548 () => { 549 document.body.classList.add("initialized"); 550 }, 551 { once: true } 552 ); 553 } else { 554 document.body.classList.add("initialized"); 555 } 556 } 557 558 window.addEventListener("DOMContentLoaded", () => { 559 SearchWidget.init(); 560 MessageArea.init(); 561 SurveyArea.init(); 562 YecArea.init(); 563 564 gLoaded = true; 565 maybeComplete(); 566 }); 567 568 window.addEventListener("InitialData", event => { 569 const { 570 torConnectEnabled, 571 isStable, 572 searchOnionize, 573 messageData, 574 surveyDismissVersion, 575 appLocale, 576 dismissYEC, 577 } = event.detail; 578 SearchWidget.setOnionizeState(!!searchOnionize); 579 MessageArea.setMessageData(messageData, !!isStable, !!torConnectEnabled); 580 SurveyArea.potentiallyShow(surveyDismissVersion, isStable, appLocale); 581 YecArea.potentiallyShow(dismissYEC, isStable, appLocale); 582 583 gInitialData = true; 584 maybeComplete(); 585 }); 586 587 window.addEventListener("DismissYEC", () => { 588 // User closed the banner in another about:tor instance. 589 YecArea.dismiss(); 590 });