richard pushed to branch tor-browser-115.7.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits:
-
e16caa30
by Henry Wilkes at 2024-01-29T18:34:30+00:00
-
6fe742ee
by Henry Wilkes at 2024-01-29T18:34:30+00:00
-
7abc2cc1
by Henry Wilkes at 2024-01-29T18:34:30+00:00
-
fd4565d1
by Henry Wilkes at 2024-01-29T18:34:30+00:00
12 changed files:
- browser/components/preferences/preferences.xhtml
- + browser/components/torpreferences/content/bridgemoji/BridgeEmoji.js
- browser/components/torpreferences/content/connectionPane.js
- browser/components/torpreferences/content/connectionPane.xhtml
- browser/components/torpreferences/content/provideBridgeDialog.js
- browser/components/torpreferences/content/provideBridgeDialog.xhtml
- browser/components/torpreferences/content/torPreferences.css
- browser/components/torpreferences/jar.mn
- browser/locales/en-US/browser/tor-browser.ftl
- toolkit/modules/TorSettings.sys.mjs
- toolkit/modules/TorStrings.sys.mjs
- toolkit/torbutton/chrome/locale/en-US/settings.properties
Changes:
| ... | ... | @@ -70,6 +70,7 @@ |
| 70 | 70 | <script type="module" src="chrome://global/content/elements/moz-support-link.mjs"/>
|
| 71 | 71 | <script src="chrome://browser/content/migration/migration-wizard.mjs" type="module"></script>
|
| 72 | 72 | <script type="module" src="">"chrome://global/content/elements/moz-toggle.mjs"/>
|
| 73 | + <script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js"/>
|
|
| 73 | 74 | </head>
|
| 74 | 75 | |
| 75 | 76 | <html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
| 1 | +"use strict";
|
|
| 2 | + |
|
| 3 | +{
|
|
| 4 | + /**
|
|
| 5 | + * Element to display a single bridge emoji, with a localized name.
|
|
| 6 | + */
|
|
| 7 | + class BridgeEmoji extends HTMLElement {
|
|
| 8 | + static #activeInstances = new Set();
|
|
| 9 | + static #observer(subject, topic, data) {
|
|
| 10 | + if (topic === "intl:app-locales-changed") {
|
|
| 11 | + BridgeEmoji.#updateEmojiLangCode();
|
|
| 12 | + }
|
|
| 13 | + }
|
|
| 14 | + |
|
| 15 | + static #addActiveInstance(inst) {
|
|
| 16 | + if (this.#activeInstances.size === 0) {
|
|
| 17 | + Services.obs.addObserver(this.#observer, "intl:app-locales-changed");
|
|
| 18 | + this.#updateEmojiLangCode();
|
|
| 19 | + }
|
|
| 20 | + this.#activeInstances.add(inst);
|
|
| 21 | + }
|
|
| 22 | + |
|
| 23 | + static #removeActiveInstance(inst) {
|
|
| 24 | + this.#activeInstances.delete(inst);
|
|
| 25 | + if (this.#activeInstances.size === 0) {
|
|
| 26 | + Services.obs.removeObserver(this.#observer, "intl:app-locales-changed");
|
|
| 27 | + }
|
|
| 28 | + }
|
|
| 29 | + |
|
| 30 | + /**
|
|
| 31 | + * The language code for emoji annotations.
|
|
| 32 | + *
|
|
| 33 | + * null if unset.
|
|
| 34 | + *
|
|
| 35 | + * @type {string?}
|
|
| 36 | + */
|
|
| 37 | + static #emojiLangCode = null;
|
|
| 38 | + /**
|
|
| 39 | + * A promise that resolves to two JSON structures for bridge-emojis.json and
|
|
| 40 | + * annotations.json, respectively.
|
|
| 41 | + *
|
|
| 42 | + * @type {Promise}
|
|
| 43 | + */
|
|
| 44 | + static #emojiPromise = Promise.all([
|
|
| 45 | + fetch(
|
|
| 46 | + "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
|
|
| 47 | + ).then(response => response.json()),
|
|
| 48 | + fetch(
|
|
| 49 | + "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
|
|
| 50 | + ).then(response => response.json()),
|
|
| 51 | + ]);
|
|
| 52 | + |
|
| 53 | + static #unknownStringPromise = null;
|
|
| 54 | + |
|
| 55 | + /**
|
|
| 56 | + * Update #emojiLangCode.
|
|
| 57 | + */
|
|
| 58 | + static async #updateEmojiLangCode() {
|
|
| 59 | + let langCode;
|
|
| 60 | + const emojiAnnotations = (await BridgeEmoji.#emojiPromise)[1];
|
|
| 61 | + // Find the first desired locale we have annotations for.
|
|
| 62 | + // Add "en" as a fallback.
|
|
| 63 | + for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) {
|
|
| 64 | + langCode = bcp47;
|
|
| 65 | + if (langCode in emojiAnnotations) {
|
|
| 66 | + break;
|
|
| 67 | + }
|
|
| 68 | + // Remove everything after the dash, if there is one.
|
|
| 69 | + langCode = bcp47.replace(/-.*/, "");
|
|
| 70 | + if (langCode in emojiAnnotations) {
|
|
| 71 | + break;
|
|
| 72 | + }
|
|
| 73 | + }
|
|
| 74 | + if (langCode !== this.#emojiLangCode) {
|
|
| 75 | + this.#emojiLangCode = langCode;
|
|
| 76 | + this.#unknownStringPromise = document.l10n.formatValue(
|
|
| 77 | + "tor-bridges-emoji-unknown"
|
|
| 78 | + );
|
|
| 79 | + for (const inst of this.#activeInstances) {
|
|
| 80 | + inst.update();
|
|
| 81 | + }
|
|
| 82 | + }
|
|
| 83 | + }
|
|
| 84 | + |
|
| 85 | + /**
|
|
| 86 | + * Update the bridge emoji to show their corresponding emoji with an
|
|
| 87 | + * annotation that matches the current locale.
|
|
| 88 | + */
|
|
| 89 | + async update() {
|
|
| 90 | + if (!this.#active) {
|
|
| 91 | + return;
|
|
| 92 | + }
|
|
| 93 | + |
|
| 94 | + if (!BridgeEmoji.#emojiLangCode) {
|
|
| 95 | + // No lang code yet, wait until it is updated.
|
|
| 96 | + return;
|
|
| 97 | + }
|
|
| 98 | + |
|
| 99 | + const doc = this.ownerDocument;
|
|
| 100 | + const [unknownString, [emojiList, emojiAnnotations]] = await Promise.all([
|
|
| 101 | + BridgeEmoji.#unknownStringPromise,
|
|
| 102 | + BridgeEmoji.#emojiPromise,
|
|
| 103 | + ]);
|
|
| 104 | + |
|
| 105 | + const emoji = emojiList[this.#index];
|
|
| 106 | + let emojiName;
|
|
| 107 | + if (!emoji) {
|
|
| 108 | + // Unexpected.
|
|
| 109 | + this.#img.removeAttribute("src");
|
|
| 110 | + } else {
|
|
| 111 | + const cp = emoji.codePointAt(0).toString(16);
|
|
| 112 | + this.#img.setAttribute(
|
|
| 113 | + "src",
|
|
| 114 | + `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
|
|
| 115 | + );
|
|
| 116 | + emojiName = emojiAnnotations[BridgeEmoji.#emojiLangCode][cp];
|
|
| 117 | + }
|
|
| 118 | + if (!emojiName) {
|
|
| 119 | + doc.defaultView.console.error(`No emoji for index ${this.#index}`);
|
|
| 120 | + emojiName = unknownString;
|
|
| 121 | + }
|
|
| 122 | + doc.l10n.setAttributes(this, "tor-bridges-emoji-cell", {
|
|
| 123 | + emojiName,
|
|
| 124 | + });
|
|
| 125 | + }
|
|
| 126 | + |
|
| 127 | + /**
|
|
| 128 | + * The index for this bridge emoji.
|
|
| 129 | + *
|
|
| 130 | + * @type {integer?}
|
|
| 131 | + */
|
|
| 132 | + #index = null;
|
|
| 133 | + /**
|
|
| 134 | + * Whether we are active (i.e. in the DOM).
|
|
| 135 | + *
|
|
| 136 | + * @type {boolean}
|
|
| 137 | + */
|
|
| 138 | + #active = false;
|
|
| 139 | + /**
|
|
| 140 | + * The image element.
|
|
| 141 | + *
|
|
| 142 | + * @type {HTMLImgElement?}
|
|
| 143 | + */
|
|
| 144 | + #img = null;
|
|
| 145 | + |
|
| 146 | + constructor(index) {
|
|
| 147 | + super();
|
|
| 148 | + this.#index = index;
|
|
| 149 | + }
|
|
| 150 | + |
|
| 151 | + connectedCallback() {
|
|
| 152 | + if (!this.#img) {
|
|
| 153 | + this.#img = this.ownerDocument.createElement("img");
|
|
| 154 | + this.#img.classList.add("tor-bridges-emoji-icon");
|
|
| 155 | + this.#img.setAttribute("alt", "");
|
|
| 156 | + this.appendChild(this.#img);
|
|
| 157 | + }
|
|
| 158 | + |
|
| 159 | + this.#active = true;
|
|
| 160 | + BridgeEmoji.#addActiveInstance(this);
|
|
| 161 | + this.update();
|
|
| 162 | + }
|
|
| 163 | + |
|
| 164 | + disconnectedCallback() {
|
|
| 165 | + this.#active = false;
|
|
| 166 | + BridgeEmoji.#removeActiveInstance(this);
|
|
| 167 | + }
|
|
| 168 | + |
|
| 169 | + /**
|
|
| 170 | + * Create four bridge emojis for the given address.
|
|
| 171 | + *
|
|
| 172 | + * @param {string} bridgeLine - The bridge address.
|
|
| 173 | + *
|
|
| 174 | + * @returns {BridgeEmoji[4]} - The bridge emoji elements.
|
|
| 175 | + */
|
|
| 176 | + static createForAddress(bridgeLine) {
|
|
| 177 | + // JS uses UTF-16. While most of these emojis are surrogate pairs, a few
|
|
| 178 | + // ones fit one UTF-16 character. So we could not use neither indices,
|
|
| 179 | + // nor substr, nor some function to split the string.
|
|
| 180 | + // FNV-1a implementation that is compatible with other languages
|
|
| 181 | + const prime = 0x01000193;
|
|
| 182 | + const offset = 0x811c9dc5;
|
|
| 183 | + let hash = offset;
|
|
| 184 | + const encoder = new TextEncoder();
|
|
| 185 | + for (const byte of encoder.encode(bridgeLine)) {
|
|
| 186 | + hash = Math.imul(hash ^ byte, prime);
|
|
| 187 | + }
|
|
| 188 | + |
|
| 189 | + return [
|
|
| 190 | + ((hash & 0x7f000000) >> 24) | (hash < 0 ? 0x80 : 0),
|
|
| 191 | + (hash & 0x00ff0000) >> 16,
|
|
| 192 | + (hash & 0x0000ff00) >> 8,
|
|
| 193 | + hash & 0x000000ff,
|
|
| 194 | + ].map(index => new BridgeEmoji(index));
|
|
| 195 | + }
|
|
| 196 | + }
|
|
| 197 | + |
|
| 198 | + customElements.define("tor-bridge-emoji", BridgeEmoji);
|
|
| 199 | +} |
| ... | ... | @@ -299,12 +299,10 @@ const gBridgeGrid = { |
| 299 | 299 | |
| 300 | 300 | this._active = true;
|
| 301 | 301 | |
| 302 | - Services.obs.addObserver(this, "intl:app-locales-changed");
|
|
| 303 | 302 | Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
|
| 304 | 303 | |
| 305 | 304 | this._grid.classList.add("grid-active");
|
| 306 | 305 | |
| 307 | - this._updateEmojiLangCode();
|
|
| 308 | 306 | this._updateConnectedBridge();
|
| 309 | 307 | },
|
| 310 | 308 | |
| ... | ... | @@ -322,7 +320,6 @@ const gBridgeGrid = { |
| 322 | 320 | |
| 323 | 321 | this._grid.classList.remove("grid-active");
|
| 324 | 322 | |
| 325 | - Services.obs.removeObserver(this, "intl:app-locales-changed");
|
|
| 326 | 323 | Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
|
| 327 | 324 | },
|
| 328 | 325 | |
| ... | ... | @@ -337,9 +334,6 @@ const gBridgeGrid = { |
| 337 | 334 | this._updateRows();
|
| 338 | 335 | }
|
| 339 | 336 | break;
|
| 340 | - case "intl:app-locales-changed":
|
|
| 341 | - this._updateEmojiLangCode();
|
|
| 342 | - break;
|
|
| 343 | 337 | case TorProviderTopics.BridgeChanged:
|
| 344 | 338 | this._updateConnectedBridge();
|
| 345 | 339 | break;
|
| ... | ... | @@ -573,97 +567,6 @@ const gBridgeGrid = { |
| 573 | 567 | }
|
| 574 | 568 | },
|
| 575 | 569 | |
| 576 | - /**
|
|
| 577 | - * The language code for emoji annotations.
|
|
| 578 | - *
|
|
| 579 | - * null if unset.
|
|
| 580 | - *
|
|
| 581 | - * @type {string?}
|
|
| 582 | - */
|
|
| 583 | - _emojiLangCode: null,
|
|
| 584 | - /**
|
|
| 585 | - * A promise that resolves to two JSON structures for bridge-emojis.json and
|
|
| 586 | - * annotations.json, respectively.
|
|
| 587 | - *
|
|
| 588 | - * @type {Promise}
|
|
| 589 | - */
|
|
| 590 | - _emojiPromise: Promise.all([
|
|
| 591 | - fetch(
|
|
| 592 | - "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
|
|
| 593 | - ).then(response => response.json()),
|
|
| 594 | - fetch(
|
|
| 595 | - "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
|
|
| 596 | - ).then(response => response.json()),
|
|
| 597 | - ]),
|
|
| 598 | - |
|
| 599 | - /**
|
|
| 600 | - * Update _emojiLangCode.
|
|
| 601 | - */
|
|
| 602 | - async _updateEmojiLangCode() {
|
|
| 603 | - let langCode;
|
|
| 604 | - const emojiAnnotations = (await this._emojiPromise)[1];
|
|
| 605 | - // Find the first desired locale we have annotations for.
|
|
| 606 | - // Add "en" as a fallback.
|
|
| 607 | - for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) {
|
|
| 608 | - langCode = bcp47;
|
|
| 609 | - if (langCode in emojiAnnotations) {
|
|
| 610 | - break;
|
|
| 611 | - }
|
|
| 612 | - // Remove everything after the dash, if there is one.
|
|
| 613 | - langCode = bcp47.replace(/-.*/, "");
|
|
| 614 | - if (langCode in emojiAnnotations) {
|
|
| 615 | - break;
|
|
| 616 | - }
|
|
| 617 | - }
|
|
| 618 | - if (langCode !== this._emojiLangCode) {
|
|
| 619 | - this._emojiLangCode = langCode;
|
|
| 620 | - for (const row of this._rows) {
|
|
| 621 | - this._updateRowEmojis(row);
|
|
| 622 | - }
|
|
| 623 | - }
|
|
| 624 | - },
|
|
| 625 | - |
|
| 626 | - /**
|
|
| 627 | - * Update the bridge emojis to show their corresponding emoji with an
|
|
| 628 | - * annotation that matches the current locale.
|
|
| 629 | - *
|
|
| 630 | - * @param {BridgeGridRow} row - The row to update the emojis of.
|
|
| 631 | - */
|
|
| 632 | - async _updateRowEmojis(row) {
|
|
| 633 | - if (!this._emojiLangCode) {
|
|
| 634 | - // No lang code yet, wait until it is updated.
|
|
| 635 | - return;
|
|
| 636 | - }
|
|
| 637 | - |
|
| 638 | - const [emojiList, emojiAnnotations] = await this._emojiPromise;
|
|
| 639 | - const unknownString = await document.l10n.formatValue(
|
|
| 640 | - "tor-bridges-emoji-unknown"
|
|
| 641 | - );
|
|
| 642 | - |
|
| 643 | - for (const { cell, img, index } of row.emojis) {
|
|
| 644 | - const emoji = emojiList[index];
|
|
| 645 | - let emojiName;
|
|
| 646 | - if (!emoji) {
|
|
| 647 | - // Unexpected.
|
|
| 648 | - img.removeAttribute("src");
|
|
| 649 | - } else {
|
|
| 650 | - const cp = emoji.codePointAt(0).toString(16);
|
|
| 651 | - img.setAttribute(
|
|
| 652 | - "src",
|
|
| 653 | - `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
|
|
| 654 | - );
|
|
| 655 | - emojiName = emojiAnnotations[this._emojiLangCode][cp];
|
|
| 656 | - }
|
|
| 657 | - if (!emojiName) {
|
|
| 658 | - console.error(`No emoji for index ${index}`);
|
|
| 659 | - emojiName = unknownString;
|
|
| 660 | - }
|
|
| 661 | - document.l10n.setAttributes(cell, "tor-bridges-emoji-cell", {
|
|
| 662 | - emojiName,
|
|
| 663 | - });
|
|
| 664 | - }
|
|
| 665 | - },
|
|
| 666 | - |
|
| 667 | 570 | /**
|
| 668 | 571 | * Create a new row for the grid.
|
| 669 | 572 | *
|
| ... | ... | @@ -688,23 +591,14 @@ const gBridgeGrid = { |
| 688 | 591 | };
|
| 689 | 592 | |
| 690 | 593 | const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block");
|
| 691 | - row.emojis = makeBridgeId(bridgeLine).map(index => {
|
|
| 692 | - const cell = document.createElement("span");
|
|
| 693 | - // Each emoji is its own cell, we rely on the fact that makeBridgeId
|
|
| 694 | - // always returns four indices.
|
|
| 594 | + const BridgeEmoji = customElements.get("tor-bridge-emoji");
|
|
| 595 | + for (const cell of BridgeEmoji.createForAddress(bridgeLine)) {
|
|
| 596 | + // Each emoji is its own cell, we rely on the fact that createForAddress
|
|
| 597 | + // always returns four elements.
|
|
| 695 | 598 | cell.setAttribute("role", "gridcell");
|
| 696 | 599 | cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
|
| 697 | - |
|
| 698 | - const img = document.createElement("img");
|
|
| 699 | - img.classList.add("tor-bridges-emoji-icon");
|
|
| 700 | - // Accessible name will be set on the cell itself.
|
|
| 701 | - img.setAttribute("alt", "");
|
|
| 702 | - |
|
| 703 | - cell.appendChild(img);
|
|
| 704 | - emojiBlock.appendChild(cell);
|
|
| 705 | - // Image and text is set in _updateRowEmojis.
|
|
| 706 | - return { cell, img, index };
|
|
| 707 | - });
|
|
| 600 | + emojiBlock.append(cell);
|
|
| 601 | + }
|
|
| 708 | 602 | |
| 709 | 603 | for (const [columnIndex, element] of row.element
|
| 710 | 604 | .querySelectorAll(".tor-bridges-grid-cell")
|
| ... | ... | @@ -735,7 +629,6 @@ const gBridgeGrid = { |
| 735 | 629 | this._initRowMenu(row);
|
| 736 | 630 | |
| 737 | 631 | this._updateRowStatus(row);
|
| 738 | - this._updateRowEmojis(row);
|
|
| 739 | 632 | return row;
|
| 740 | 633 | },
|
| 741 | 634 | |
| ... | ... | @@ -1870,13 +1763,13 @@ const gBridgeSettings = { |
| 1870 | 1763 | "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml",
|
| 1871 | 1764 | { mode },
|
| 1872 | 1765 | result => {
|
| 1873 | - if (!result.bridgeStrings) {
|
|
| 1766 | + if (!result.bridges?.length) {
|
|
| 1874 | 1767 | return null;
|
| 1875 | 1768 | }
|
| 1876 | 1769 | return setTorSettings(() => {
|
| 1877 | 1770 | TorSettings.bridges.enabled = true;
|
| 1878 | 1771 | TorSettings.bridges.source = TorBridgeSource.UserProvided;
|
| 1879 | - TorSettings.bridges.bridge_strings = result.bridgeStrings;
|
|
| 1772 | + TorSettings.bridges.bridge_strings = result.bridges;
|
|
| 1880 | 1773 | });
|
| 1881 | 1774 | }
|
| 1882 | 1775 | );
|
| ... | ... | @@ -2292,32 +2185,3 @@ const gConnectionPane = (function () { |
| 2292 | 2185 | };
|
| 2293 | 2186 | return retval;
|
| 2294 | 2187 | })(); /* gConnectionPane */ |
| 2295 | - |
|
| 2296 | -/**
|
|
| 2297 | - * Convert the given bridgeString into an array of emoji indices between 0 and
|
|
| 2298 | - * 255.
|
|
| 2299 | - *
|
|
| 2300 | - * @param {string} bridgeString - The bridge string.
|
|
| 2301 | - *
|
|
| 2302 | - * @returns {integer[]} - A list of emoji indices between 0 and 255.
|
|
| 2303 | - */
|
|
| 2304 | -function makeBridgeId(bridgeString) {
|
|
| 2305 | - // JS uses UTF-16. While most of these emojis are surrogate pairs, a few
|
|
| 2306 | - // ones fit one UTF-16 character. So we could not use neither indices,
|
|
| 2307 | - // nor substr, nor some function to split the string.
|
|
| 2308 | - // FNV-1a implementation that is compatible with other languages
|
|
| 2309 | - const prime = 0x01000193;
|
|
| 2310 | - const offset = 0x811c9dc5;
|
|
| 2311 | - let hash = offset;
|
|
| 2312 | - const encoder = new TextEncoder();
|
|
| 2313 | - for (const byte of encoder.encode(bridgeString)) {
|
|
| 2314 | - hash = Math.imul(hash ^ byte, prime);
|
|
| 2315 | - }
|
|
| 2316 | - |
|
| 2317 | - return [
|
|
| 2318 | - ((hash & 0x7f000000) >> 24) | (hash < 0 ? 0x80 : 0),
|
|
| 2319 | - (hash & 0x00ff0000) >> 16,
|
|
| 2320 | - (hash & 0x0000ff00) >> 8,
|
|
| 2321 | - hash & 0x000000ff,
|
|
| 2322 | - ];
|
|
| 2323 | -} |
| ... | ... | @@ -218,6 +218,7 @@ |
| 218 | 218 | </html:div>
|
| 219 | 219 | <html:div
|
| 220 | 220 | id="tor-bridges-grid-display"
|
| 221 | + class="tor-bridges-grid"
|
|
| 221 | 222 | role="grid"
|
| 222 | 223 | aria-labelledby="tor-bridges-current-heading"
|
| 223 | 224 | ></html:div>
|
| ... | ... | @@ -4,14 +4,17 @@ const { TorStrings } = ChromeUtils.importESModule( |
| 4 | 4 | "resource://gre/modules/TorStrings.sys.mjs"
|
| 5 | 5 | );
|
| 6 | 6 | |
| 7 | -const { TorSettings, TorBridgeSource } = ChromeUtils.importESModule(
|
|
| 8 | - "resource://gre/modules/TorSettings.sys.mjs"
|
|
| 9 | -);
|
|
| 7 | +const { TorSettings, TorBridgeSource, validateBridgeLines } =
|
|
| 8 | + ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
|
|
| 10 | 9 | |
| 11 | 10 | const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule(
|
| 12 | 11 | "resource://gre/modules/TorConnect.sys.mjs"
|
| 13 | 12 | );
|
| 14 | 13 | |
| 14 | +const { TorParsers } = ChromeUtils.importESModule(
|
|
| 15 | + "resource://gre/modules/TorParsers.sys.mjs"
|
|
| 16 | +);
|
|
| 17 | + |
|
| 15 | 18 | const gProvideBridgeDialog = {
|
| 16 | 19 | init() {
|
| 17 | 20 | this._result = window.arguments[0];
|
| ... | ... | @@ -33,72 +36,264 @@ const gProvideBridgeDialog = { |
| 33 | 36 | |
| 34 | 37 | document.l10n.setAttributes(document.documentElement, titleId);
|
| 35 | 38 | |
| 36 | - const learnMore = document.createXULElement("label");
|
|
| 37 | - learnMore.className = "learnMore text-link";
|
|
| 38 | - learnMore.setAttribute("is", "text-link");
|
|
| 39 | - learnMore.setAttribute("value", TorStrings.settings.learnMore);
|
|
| 40 | - learnMore.addEventListener("click", () => {
|
|
| 41 | - window.top.openTrustedLinkIn(
|
|
| 42 | - TorStrings.settings.learnMoreBridgesURL,
|
|
| 43 | - "tab"
|
|
| 44 | - );
|
|
| 45 | - });
|
|
| 46 | - |
|
| 47 | - const pieces = TorStrings.settings.provideBridgeDescription.split("%S");
|
|
| 48 | - document
|
|
| 49 | - .getElementById("torPreferences-provideBridge-description")
|
|
| 50 | - .replaceChildren(pieces[0], learnMore, pieces[1] || "");
|
|
| 39 | + document.l10n.setAttributes(
|
|
| 40 | + document.getElementById("user-provide-bridge-textarea-label"),
|
|
| 41 | + // TODO change string when we can also accept Lox share codes.
|
|
| 42 | + "user-provide-bridge-dialog-textarea-addresses-label"
|
|
| 43 | + );
|
|
| 51 | 44 | |
| 52 | - this._textarea = document.getElementById(
|
|
| 53 | - "torPreferences-provideBridge-textarea"
|
|
| 45 | + this._dialog = document.getElementById("user-provide-bridge-dialog");
|
|
| 46 | + this._acceptButton = this._dialog.getButton("accept");
|
|
| 47 | + this._textarea = document.getElementById("user-provide-bridge-textarea");
|
|
| 48 | + this._errorEl = document.getElementById(
|
|
| 49 | + "user-provide-bridge-error-message"
|
|
| 50 | + );
|
|
| 51 | + this._resultDescription = document.getElementById(
|
|
| 52 | + "user-provide-result-description"
|
|
| 53 | + );
|
|
| 54 | + this._bridgeGrid = document.getElementById(
|
|
| 55 | + "user-provide-bridge-grid-display"
|
|
| 54 | 56 | );
|
| 55 | - this._textarea.setAttribute(
|
|
| 56 | - "placeholder",
|
|
| 57 | - TorStrings.settings.provideBridgePlaceholder
|
|
| 57 | + this._rowTemplate = document.getElementById(
|
|
| 58 | + "user-provide-bridge-row-template"
|
|
| 58 | 59 | );
|
| 59 | 60 | |
| 60 | - this._textarea.addEventListener("input", () => this.onValueChange());
|
|
| 61 | - if (TorSettings.bridges.source == TorBridgeSource.UserProvided) {
|
|
| 62 | - this._textarea.value = TorSettings.bridges.bridge_strings.join("\n");
|
|
| 61 | + if (mode === "edit") {
|
|
| 62 | + // Only expected if the bridge source is UseProvided, but verify to be
|
|
| 63 | + // sure.
|
|
| 64 | + if (TorSettings.bridges.source == TorBridgeSource.UserProvided) {
|
|
| 65 | + this._textarea.value = TorSettings.bridges.bridge_strings.join("\n");
|
|
| 66 | + }
|
|
| 67 | + } else {
|
|
| 68 | + // Set placeholder if not editing.
|
|
| 69 | + document.l10n.setAttributes(
|
|
| 70 | + this._textarea,
|
|
| 71 | + // TODO: change string when we can also accept Lox share codes.
|
|
| 72 | + "user-provide-bridge-dialog-textarea-addresses"
|
|
| 73 | + );
|
|
| 63 | 74 | }
|
| 64 | 75 | |
| 65 | - const dialog = document.getElementById(
|
|
| 66 | - "torPreferences-provideBridge-dialog"
|
|
| 67 | - );
|
|
| 68 | - dialog.addEventListener("dialogaccept", e => {
|
|
| 69 | - this._result.accepted = true;
|
|
| 70 | - });
|
|
| 76 | + this._textarea.addEventListener("input", () => this.onValueChange());
|
|
| 71 | 77 | |
| 72 | - this._acceptButton = dialog.getButton("accept");
|
|
| 78 | + this._dialog.addEventListener("dialogaccept", event =>
|
|
| 79 | + this.onDialogAccept(event)
|
|
| 80 | + );
|
|
| 73 | 81 | |
| 74 | 82 | Services.obs.addObserver(this, TorConnectTopics.StateChange);
|
| 75 | 83 | |
| 76 | - this.onValueChange();
|
|
| 77 | - this.onAcceptStateChange();
|
|
| 84 | + this.setPage("entry");
|
|
| 85 | + this.checkValue();
|
|
| 78 | 86 | },
|
| 79 | 87 | |
| 80 | 88 | uninit() {
|
| 81 | 89 | Services.obs.removeObserver(this, TorConnectTopics.StateChange);
|
| 82 | 90 | },
|
| 83 | 91 | |
| 92 | + /**
|
|
| 93 | + * Set the page to display.
|
|
| 94 | + *
|
|
| 95 | + * @param {string} page - The page to show.
|
|
| 96 | + */
|
|
| 97 | + setPage(page) {
|
|
| 98 | + this._page = page;
|
|
| 99 | + this._dialog.classList.toggle("show-entry-page", page === "entry");
|
|
| 100 | + this._dialog.classList.toggle("show-result-page", page === "result");
|
|
| 101 | + if (page === "entry") {
|
|
| 102 | + this._textarea.focus();
|
|
| 103 | + } else {
|
|
| 104 | + // Move focus to the <xul:window> element.
|
|
| 105 | + // In particular, we do not want to keep the focus on the (same) accept
|
|
| 106 | + // button (with now different text).
|
|
| 107 | + document.documentElement.focus();
|
|
| 108 | + }
|
|
| 109 | + |
|
| 110 | + this.updateAcceptDisabled();
|
|
| 111 | + this.onAcceptStateChange();
|
|
| 112 | + },
|
|
| 113 | + |
|
| 114 | + /**
|
|
| 115 | + * Callback for whenever the input value changes.
|
|
| 116 | + */
|
|
| 84 | 117 | onValueChange() {
|
| 85 | - // TODO: Do some proper value parsing and error reporting. See
|
|
| 86 | - // tor-browser#40552.
|
|
| 87 | - const value = this._textarea.value.trim();
|
|
| 88 | - this._acceptButton.disabled = !value;
|
|
| 89 | - this._result.bridgeStrings = value;
|
|
| 118 | + this.updateAcceptDisabled();
|
|
| 119 | + // Reset errors whenever the value changes.
|
|
| 120 | + this.updateError(null);
|
|
| 90 | 121 | },
|
| 91 | 122 | |
| 123 | + /**
|
|
| 124 | + * Callback for whenever the accept button may need to change.
|
|
| 125 | + */
|
|
| 92 | 126 | onAcceptStateChange() {
|
| 93 | - const connect = TorConnect.canBeginBootstrap;
|
|
| 94 | - this._result.connect = connect;
|
|
| 95 | - |
|
| 96 | - this._acceptButton.setAttribute(
|
|
| 97 | - "label",
|
|
| 98 | - connect
|
|
| 99 | - ? TorStrings.settings.bridgeButtonConnect
|
|
| 100 | - : TorStrings.settings.bridgeButtonAccept
|
|
| 127 | + if (this._page === "entry") {
|
|
| 128 | + document.l10n.setAttributes(
|
|
| 129 | + this._acceptButton,
|
|
| 130 | + "user-provide-bridge-dialog-next-button"
|
|
| 131 | + );
|
|
| 132 | + this._result.connect = false;
|
|
| 133 | + } else {
|
|
| 134 | + this._acceptButton.removeAttribute("data-l10n-id");
|
|
| 135 | + const connect = TorConnect.canBeginBootstrap;
|
|
| 136 | + this._result.connect = connect;
|
|
| 137 | + |
|
| 138 | + this._acceptButton.setAttribute(
|
|
| 139 | + "label",
|
|
| 140 | + connect
|
|
| 141 | + ? TorStrings.settings.bridgeButtonConnect
|
|
| 142 | + : TorStrings.settings.bridgeButtonAccept
|
|
| 143 | + );
|
|
| 144 | + }
|
|
| 145 | + },
|
|
| 146 | + |
|
| 147 | + /**
|
|
| 148 | + * Callback for whenever the accept button's might need to be disabled.
|
|
| 149 | + */
|
|
| 150 | + updateAcceptDisabled() {
|
|
| 151 | + this._acceptButton.disabled =
|
|
| 152 | + this._page === "entry" && validateBridgeLines(this._textarea.value).empty;
|
|
| 153 | + },
|
|
| 154 | + |
|
| 155 | + /**
|
|
| 156 | + * Callback for when the accept button is pressed.
|
|
| 157 | + *
|
|
| 158 | + * @param {Event} event - The dialogaccept event.
|
|
| 159 | + */
|
|
| 160 | + onDialogAccept(event) {
|
|
| 161 | + if (this._page === "result") {
|
|
| 162 | + this._result.accepted = true;
|
|
| 163 | + // Continue to close the dialog.
|
|
| 164 | + return;
|
|
| 165 | + }
|
|
| 166 | + // Prevent closing the dialog.
|
|
| 167 | + event.preventDefault();
|
|
| 168 | + |
|
| 169 | + const bridges = this.checkValue();
|
|
| 170 | + if (!bridges.length) {
|
|
| 171 | + // Not valid
|
|
| 172 | + return;
|
|
| 173 | + }
|
|
| 174 | + this._result.bridges = bridges;
|
|
| 175 | + this.updateResult();
|
|
| 176 | + this.setPage("result");
|
|
| 177 | + },
|
|
| 178 | + |
|
| 179 | + /**
|
|
| 180 | + * The current timeout for updating the error.
|
|
| 181 | + *
|
|
| 182 | + * @type {integer?}
|
|
| 183 | + */
|
|
| 184 | + _updateErrorTimeout: null,
|
|
| 185 | + |
|
| 186 | + /**
|
|
| 187 | + * Update the displayed error.
|
|
| 188 | + *
|
|
| 189 | + * @param {object?} error - The error to show, or null if no error should be
|
|
| 190 | + * shown. Should include the "type" property.
|
|
| 191 | + */
|
|
| 192 | + updateError(error) {
|
|
| 193 | + // First clear the existing error.
|
|
| 194 | + if (this._updateErrorTimeout !== null) {
|
|
| 195 | + clearTimeout(this._updateErrorTimeout);
|
|
| 196 | + }
|
|
| 197 | + this._updateErrorTimeout = null;
|
|
| 198 | + this._errorEl.removeAttribute("data-l10n-id");
|
|
| 199 | + this._errorEl.textContent = "";
|
|
| 200 | + if (error) {
|
|
| 201 | + this._textarea.setAttribute("aria-invalid", "true");
|
|
| 202 | + } else {
|
|
| 203 | + this._textarea.removeAttribute("aria-invalid");
|
|
| 204 | + }
|
|
| 205 | + this._textarea.classList.toggle("invalid-input", !!error);
|
|
| 206 | + this._errorEl.classList.toggle("show-error", !!error);
|
|
| 207 | + |
|
| 208 | + if (!error) {
|
|
| 209 | + return;
|
|
| 210 | + }
|
|
| 211 | + |
|
| 212 | + let errorId;
|
|
| 213 | + let errorArgs;
|
|
| 214 | + switch (error.type) {
|
|
| 215 | + case "invalid-address":
|
|
| 216 | + errorId = "user-provide-bridge-dialog-address-error";
|
|
| 217 | + errorArgs = { line: error.line };
|
|
| 218 | + break;
|
|
| 219 | + }
|
|
| 220 | + |
|
| 221 | + // Wait a small amount of time to actually set the textContent. Otherwise
|
|
| 222 | + // the screen reader (tested with Orca) may not pick up on the change in
|
|
| 223 | + // text.
|
|
| 224 | + this._updateErrorTimeout = setTimeout(() => {
|
|
| 225 | + document.l10n.setAttributes(this._errorEl, errorId, errorArgs);
|
|
| 226 | + }, 500);
|
|
| 227 | + },
|
|
| 228 | + |
|
| 229 | + /**
|
|
| 230 | + * Check the current value in the textarea.
|
|
| 231 | + *
|
|
| 232 | + * @returns {string[]} - The bridge addresses, if the entry is valid.
|
|
| 233 | + */
|
|
| 234 | + checkValue() {
|
|
| 235 | + let bridges = [];
|
|
| 236 | + let error = null;
|
|
| 237 | + const validation = validateBridgeLines(this._textarea.value);
|
|
| 238 | + if (!validation.empty) {
|
|
| 239 | + // If empty, we just disable the button, rather than show an error.
|
|
| 240 | + if (validation.errorLines.length) {
|
|
| 241 | + // Report first error.
|
|
| 242 | + error = {
|
|
| 243 | + type: "invalid-address",
|
|
| 244 | + line: validation.errorLines[0],
|
|
| 245 | + };
|
|
| 246 | + } else {
|
|
| 247 | + bridges = validation.validBridges;
|
|
| 248 | + }
|
|
| 249 | + }
|
|
| 250 | + this.updateError(error);
|
|
| 251 | + return bridges;
|
|
| 252 | + },
|
|
| 253 | + |
|
| 254 | + /**
|
|
| 255 | + * Update the shown result on the last page.
|
|
| 256 | + */
|
|
| 257 | + updateResult() {
|
|
| 258 | + document.l10n.setAttributes(
|
|
| 259 | + this._resultDescription,
|
|
| 260 | + // TODO: Use a different id when added through Lox invite.
|
|
| 261 | + "user-provide-bridge-dialog-result-addresses"
|
|
| 101 | 262 | );
|
| 263 | + |
|
| 264 | + this._bridgeGrid.replaceChildren();
|
|
| 265 | + |
|
| 266 | + for (const bridgeLine of this._result.bridges) {
|
|
| 267 | + let details;
|
|
| 268 | + try {
|
|
| 269 | + details = TorParsers.parseBridgeLine(bridgeLine);
|
|
| 270 | + } catch (e) {
|
|
| 271 | + console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
|
|
| 272 | + }
|
|
| 273 | + |
|
| 274 | + const rowEl = this._rowTemplate.content.children[0].cloneNode(true);
|
|
| 275 | + |
|
| 276 | + const emojiBlock = rowEl.querySelector(".tor-bridges-emojis-block");
|
|
| 277 | + const BridgeEmoji = customElements.get("tor-bridge-emoji");
|
|
| 278 | + for (const cell of BridgeEmoji.createForAddress(bridgeLine)) {
|
|
| 279 | + // Each emoji is its own cell, we rely on the fact that createForAddress
|
|
| 280 | + // always returns four elements.
|
|
| 281 | + cell.setAttribute("role", "gridcell");
|
|
| 282 | + cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
|
|
| 283 | + emojiBlock.append(cell);
|
|
| 284 | + }
|
|
| 285 | + |
|
| 286 | + // TODO: properly handle "vanilla" bridges?
|
|
| 287 | + document.l10n.setAttributes(
|
|
| 288 | + rowEl.querySelector(".tor-bridges-type-cell"),
|
|
| 289 | + "tor-bridges-type-prefix",
|
|
| 290 | + { type: details?.transport ?? "vanilla" }
|
|
| 291 | + );
|
|
| 292 | + |
|
| 293 | + rowEl.querySelector(".tor-bridges-address-cell").textContent = bridgeLine;
|
|
| 294 | + |
|
| 295 | + this._bridgeGrid.append(rowEl);
|
|
| 296 | + }
|
|
| 102 | 297 | },
|
| 103 | 298 | |
| 104 | 299 | observe(subject, topic, data) {
|
| ... | ... | @@ -8,22 +8,77 @@ |
| 8 | 8 | xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
| 9 | 9 | xmlns:html="http://www.w3.org/1999/xhtml"
|
| 10 | 10 | >
|
| 11 | - <dialog id="torPreferences-provideBridge-dialog" buttons="accept,cancel">
|
|
| 11 | + <dialog
|
|
| 12 | + id="user-provide-bridge-dialog"
|
|
| 13 | + buttons="accept,cancel"
|
|
| 14 | + class="show-entry-page"
|
|
| 15 | + >
|
|
| 12 | 16 | <linkset>
|
| 13 | 17 | <html:link rel="localization" href="">"browser/tor-browser.ftl" />
|
| 14 | 18 | </linkset>
|
| 15 | 19 | |
| 20 | + <script src="">"chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js" />
|
|
| 16 | 21 | <script src="chrome://browser/content/torpreferences/provideBridgeDialog.js" />
|
| 17 | 22 | |
| 18 | - <description>
|
|
| 19 | - <html:div id="torPreferences-provideBridge-description"
|
|
| 20 | - >​<br />​</html:div
|
|
| 21 | - >
|
|
| 22 | - </description>
|
|
| 23 | - <html:textarea
|
|
| 24 | - id="torPreferences-provideBridge-textarea"
|
|
| 25 | - multiline="true"
|
|
| 26 | - rows="3"
|
|
| 27 | - />
|
|
| 23 | + <html:div id="user-provide-bridge-entry-page">
|
|
| 24 | + <description id="user-provide-bridge-description">
|
|
| 25 | + <html:span
|
|
| 26 | + class="tail-with-learn-more"
|
|
| 27 | + data-l10n-id="user-provide-bridge-dialog-description"
|
|
| 28 | + ></html:span>
|
|
| 29 | + <label
|
|
| 30 | + is="text-link"
|
|
| 31 | + class="learnMore text-link"
|
|
| 32 | + href="about:manual#bridges"
|
|
| 33 | + useoriginprincipal="true"
|
|
| 34 | + data-l10n-id="user-provide-bridge-dialog-learn-more"
|
|
| 35 | + />
|
|
| 36 | + </description>
|
|
| 37 | + <html:label
|
|
| 38 | + id="user-provide-bridge-textarea-label"
|
|
| 39 | + for="user-provide-bridge-textarea"
|
|
| 40 | + ></html:label>
|
|
| 41 | + <html:textarea
|
|
| 42 | + id="user-provide-bridge-textarea"
|
|
| 43 | + multiline="true"
|
|
| 44 | + rows="3"
|
|
| 45 | + aria-describedby="user-provide-bridge-description"
|
|
| 46 | + aria-errormessage="user-provide-bridge-error-message"
|
|
| 47 | + />
|
|
| 48 | + <html:div id="user-provide-bridge-message-area">
|
|
| 49 | + <html:span
|
|
| 50 | + id="user-provide-bridge-error-message"
|
|
| 51 | + aria-live="assertive"
|
|
| 52 | + ></html:span>
|
|
| 53 | + </html:div>
|
|
| 54 | + </html:div>
|
|
| 55 | + <html:div id="user-provide-bridge-result-page">
|
|
| 56 | + <description id="user-provide-result-description" />
|
|
| 57 | + <!-- NOTE: Unlike #tor-bridge-grid-display, this element is not
|
|
| 58 | + - interactive, and not a tab-stop. So we use the "table" role rather
|
|
| 59 | + - than "grid".
|
|
| 60 | + - NOTE: Using a <html:table> would not allow us the same structural
|
|
| 61 | + - freedom, so we use a generic div and add the semantics manually. -->
|
|
| 62 | + <html:div
|
|
| 63 | + id="user-provide-bridge-grid-display"
|
|
| 64 | + class="tor-bridges-grid"
|
|
| 65 | + role="table"
|
|
| 66 | + ></html:div>
|
|
| 67 | + <html:template id="user-provide-bridge-row-template">
|
|
| 68 | + <html:div class="tor-bridges-grid-row" role="row">
|
|
| 69 | + <html:span
|
|
| 70 | + class="tor-bridges-type-cell tor-bridges-grid-cell"
|
|
| 71 | + role="gridcell"
|
|
| 72 | + ></html:span>
|
|
| 73 | + <html:span class="tor-bridges-emojis-block" role="none"></html:span>
|
|
| 74 | + <html:span class="tor-bridges-grid-end-block" role="none">
|
|
| 75 | + <html:span
|
|
| 76 | + class="tor-bridges-address-cell tor-bridges-grid-cell"
|
|
| 77 | + role="gridcell"
|
|
| 78 | + ></html:span>
|
|
| 79 | + </html:span>
|
|
| 80 | + </html:div>
|
|
| 81 | + </html:template>
|
|
| 82 | + </html:div>
|
|
| 28 | 83 | </dialog>
|
| 29 | 84 | </window> |
| ... | ... | @@ -270,11 +270,14 @@ |
| 270 | 270 | grid-area: description;
|
| 271 | 271 | }
|
| 272 | 272 | |
| 273 | -#tor-bridges-grid-display {
|
|
| 273 | +.tor-bridges-grid {
|
|
| 274 | 274 | display: grid;
|
| 275 | 275 | grid-template-columns: max-content repeat(4, max-content) 1fr;
|
| 276 | 276 | --tor-bridges-grid-column-gap: 8px;
|
| 277 | 277 | --tor-bridges-grid-column-short-gap: 4px;
|
| 278 | + /* For #tor-bridges-grid-display we want each grid item to have the same
|
|
| 279 | + * height so that their focus outlines match. */
|
|
| 280 | + align-items: stretch;
|
|
| 278 | 281 | }
|
| 279 | 282 | |
| 280 | 283 | #tor-bridges-grid-display:not(.grid-active) {
|
| ... | ... | @@ -283,11 +286,12 @@ |
| 283 | 286 | |
| 284 | 287 | .tor-bridges-grid-row {
|
| 285 | 288 | /* We want each row to act as a row of three items in the
|
| 286 | - * #tor-bridges-grid-display grid layout.
|
|
| 289 | + * .tor-bridges-grid grid layout.
|
|
| 287 | 290 | * We also want a 16px spacing between rows, and 8px spacing between columns,
|
| 288 | - * which are outside the .tor-bridges-grid-cell's border area. So that
|
|
| 289 | - * clicking these gaps will not focus any item, and their focus outlines do
|
|
| 290 | - * not overlap.
|
|
| 291 | + * which are outside the .tor-bridges-grid-cell's border area.
|
|
| 292 | + *
|
|
| 293 | + * For #tor-bridges-grid-display this should ensure that clicking these gaps
|
|
| 294 | + * will not focus any item, and their focus outlines do not overlap.
|
|
| 291 | 295 | * Moreover, we also want each row to show its .tor-bridges-options-cell when
|
| 292 | 296 | * the .tor-bridges-grid-row has :hover.
|
| 293 | 297 | *
|
| ... | ... | @@ -311,7 +315,8 @@ |
| 311 | 315 | padding-block: 8px;
|
| 312 | 316 | }
|
| 313 | 317 | |
| 314 | -.tor-bridges-grid-cell:focus-visible {
|
|
| 318 | +#tor-bridges-grid-display .tor-bridges-grid-cell:focus-visible {
|
|
| 319 | + /* #tor-bridges-grid-display has focus management for its cells. */
|
|
| 315 | 320 | outline: var(--in-content-focus-outline);
|
| 316 | 321 | outline-offset: var(--in-content-focus-outline-offset);
|
| 317 | 322 | }
|
| ... | ... | @@ -662,8 +667,77 @@ groupbox#torPreferences-bridges-group textarea { |
| 662 | 667 | }
|
| 663 | 668 | |
| 664 | 669 | /* Provide bridge dialog */
|
| 665 | -#torPreferences-provideBridge-textarea {
|
|
| 666 | - margin-top: 16px;
|
|
| 670 | + |
|
| 671 | +#user-provide-bridge-dialog:not(.show-entry-page) #user-provide-bridge-entry-page {
|
|
| 672 | + display: none;
|
|
| 673 | +}
|
|
| 674 | + |
|
| 675 | +#user-provide-bridge-dialog:not(.show-result-page) #user-provide-bridge-result-page {
|
|
| 676 | + display: none;
|
|
| 677 | +}
|
|
| 678 | + |
|
| 679 | +#user-provide-bridge-entry-page {
|
|
| 680 | + flex: 1 0 auto;
|
|
| 681 | + display: flex;
|
|
| 682 | + flex-direction: column;
|
|
| 683 | +}
|
|
| 684 | + |
|
| 685 | +#user-provide-bridge-description {
|
|
| 686 | + flex: 0 0 auto;
|
|
| 687 | +}
|
|
| 688 | + |
|
| 689 | +#user-provide-bridge-textarea-label {
|
|
| 690 | + margin-block: 16px 6px;
|
|
| 691 | + flex: 0 0 auto;
|
|
| 692 | + align-self: start;
|
|
| 693 | +}
|
|
| 694 | + |
|
| 695 | +#user-provide-bridge-textarea {
|
|
| 696 | + flex: 1 0 auto;
|
|
| 697 | + align-self: stretch;
|
|
| 698 | + line-height: 1.3;
|
|
| 699 | + margin: 0;
|
|
| 700 | +}
|
|
| 701 | + |
|
| 702 | +#user-provide-bridge-message-area {
|
|
| 703 | + flex: 0 0 auto;
|
|
| 704 | + margin-block: 8px 12px;
|
|
| 705 | + align-self: end;
|
|
| 706 | +}
|
|
| 707 | + |
|
| 708 | +#user-provide-bridge-message-area::after {
|
|
| 709 | + /* Zero width space, to ensure we are always one line high. */
|
|
| 710 | + content: "\200B";
|
|
| 711 | +}
|
|
| 712 | + |
|
| 713 | +#user-provide-bridge-textarea.invalid-input {
|
|
| 714 | + border-color: var(--in-content-danger-button-background);
|
|
| 715 | + outline-color: var(--in-content-danger-button-background);
|
|
| 716 | +}
|
|
| 717 | + |
|
| 718 | +#user-provide-bridge-error-message {
|
|
| 719 | + color: var(--in-content-error-text-color);
|
|
| 720 | +}
|
|
| 721 | + |
|
| 722 | +#user-provide-bridge-error-message.not(.show-error) {
|
|
| 723 | + display: none;
|
|
| 724 | +}
|
|
| 725 | + |
|
| 726 | +#user-provide-bridge-result-page {
|
|
| 727 | + flex: 1 1 0;
|
|
| 728 | + min-height: 0;
|
|
| 729 | + display: flex;
|
|
| 730 | + flex-direction: column;
|
|
| 731 | +}
|
|
| 732 | + |
|
| 733 | +#user-provide-result-description {
|
|
| 734 | + flex: 0 0 auto;
|
|
| 735 | +}
|
|
| 736 | + |
|
| 737 | +#user-provide-bridge-grid-display {
|
|
| 738 | + flex: 0 1 auto;
|
|
| 739 | + overflow: auto;
|
|
| 740 | + margin-block: 8px;
|
|
| 667 | 741 | }
|
| 668 | 742 | |
| 669 | 743 | /* Connection settings dialog */
|
| ... | ... | @@ -22,6 +22,7 @@ browser.jar: |
| 22 | 22 | content/browser/torpreferences/connectionPane.xhtml (content/connectionPane.xhtml)
|
| 23 | 23 | content/browser/torpreferences/torPreferences.css (content/torPreferences.css)
|
| 24 | 24 | content/browser/torpreferences/bridge-qr-onion-mask.svg (content/bridge-qr-onion-mask.svg)
|
| 25 | + content/browser/torpreferences/bridgemoji/BridgeEmoji.js (content/bridgemoji/BridgeEmoji.js)
|
|
| 25 | 26 | content/browser/torpreferences/bridgemoji/bridge-emojis.json (content/bridgemoji/bridge-emojis.json)
|
| 26 | 27 | content/browser/torpreferences/bridgemoji/annotations.json (content/bridgemoji/annotations.json)
|
| 27 | 28 | content/browser/torpreferences/bridgemoji/svgs/ (content/bridgemoji/svgs/*.svg) |
| ... | ... | @@ -176,3 +176,19 @@ user-provide-bridge-dialog-add-title = |
| 176 | 176 | # Used when the user is replacing their existing bridges with new ones.
|
| 177 | 177 | user-provide-bridge-dialog-replace-title =
|
| 178 | 178 | .title = Replace your bridges
|
| 179 | +# Description shown when adding new bridges, replacing existing bridges, or editing existing bridges.
|
|
| 180 | +user-provide-bridge-dialog-description = Use bridges provided by a trusted organisation or someone you know.
|
|
| 181 | +# "Learn more" link shown in the "Add new bridges"/"Replace your bridges" dialog.
|
|
| 182 | +user-provide-bridge-dialog-learn-more = Learn more
|
|
| 183 | +# Short accessible name for the bridge addresses text area.
|
|
| 184 | +user-provide-bridge-dialog-textarea-addresses-label = Bridge addresses
|
|
| 185 | +# Placeholder shown when adding new bridge addresses.
|
|
| 186 | +user-provide-bridge-dialog-textarea-addresses =
|
|
| 187 | + .placeholder = Paste your bridge addresses here
|
|
| 188 | +# Error shown when one of the address lines is invalid.
|
|
| 189 | +# $line (Number) - The line number for the invalid address.
|
|
| 190 | +user-provide-bridge-dialog-address-error = Incorrectly formatted bridge address on line { $line }.
|
|
| 191 | + |
|
| 192 | +user-provide-bridge-dialog-result-addresses = The following bridges were entered by you.
|
|
| 193 | +user-provide-bridge-dialog-next-button =
|
|
| 194 | + .label = Next |
| ... | ... | @@ -9,6 +9,7 @@ ChromeUtils.defineESModuleGetters(lazy, { |
| 9 | 9 | TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
|
| 10 | 10 | TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
|
| 11 | 11 | Lox: "resource://gre/modules/Lox.sys.mjs",
|
| 12 | + TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
|
|
| 12 | 13 | });
|
| 13 | 14 | |
| 14 | 15 | ChromeUtils.defineLazyGetter(lazy, "logger", () => {
|
| ... | ... | @@ -103,26 +104,61 @@ export const TorProxyType = Object.freeze({ |
| 103 | 104 | * Split a blob of bridge lines into an array with single lines.
|
| 104 | 105 | * Lines are delimited by \r\n or \n and each bridge string can also optionally
|
| 105 | 106 | * have 'bridge' at the beginning.
|
| 106 | - * We split the text by \r\n, we trim the lines, remove the bridge prefix and
|
|
| 107 | - * filter out any remaiing empty item.
|
|
| 107 | + * We split the text by \r\n, we trim the lines, remove the bridge prefix.
|
|
| 108 | 108 | *
|
| 109 | - * @param {string} aBridgeStrings The text with the lines
|
|
| 109 | + * @param {string} bridgeLines The text with the lines
|
|
| 110 | 110 | * @returns {string[]} An array where each bridge line is an item
|
| 111 | 111 | */
|
| 112 | -function parseBridgeStrings(aBridgeStrings) {
|
|
| 113 | - // replace carriage returns ('\r') with new lines ('\n')
|
|
| 114 | - aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
|
|
| 115 | - // then replace contiguous new lines ('\n') with a single one
|
|
| 116 | - aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");
|
|
| 117 | - |
|
| 112 | +function splitBridgeLines(bridgeLines) {
|
|
| 118 | 113 | // Split on the newline and for each bridge string: trim, remove starting
|
| 119 | 114 | // 'bridge' string.
|
| 120 | - // Finally, discard entries that are empty strings; empty strings could occur
|
|
| 121 | - // if we receive a new line containing only whitespace.
|
|
| 122 | - const splitStrings = aBridgeStrings.split("\n");
|
|
| 123 | - return splitStrings
|
|
| 124 | - .map(val => val.trim().replace(/^bridge\s+/i, ""))
|
|
| 125 | - .filter(bridgeString => bridgeString !== "");
|
|
| 115 | + // Replace whitespace with standard " ".
|
|
| 116 | + // NOTE: We only remove the bridge string part if it is followed by a
|
|
| 117 | + // non-whitespace.
|
|
| 118 | + return bridgeLines.split(/\r?\n/).map(val =>
|
|
| 119 | + val
|
|
| 120 | + .trim()
|
|
| 121 | + .replace(/^bridge\s+(\S)/i, "$1")
|
|
| 122 | + .replace(/\s+/, " ")
|
|
| 123 | + );
|
|
| 124 | +}
|
|
| 125 | + |
|
| 126 | +/**
|
|
| 127 | + * @typedef {Object} BridgeValidationResult
|
|
| 128 | + *
|
|
| 129 | + * @property {integer[]} errorLines - The lines that contain errors. Counting
|
|
| 130 | + * from 1.
|
|
| 131 | + * @property {boolean} empty - Whether the given string contains no bridges.
|
|
| 132 | + * @property {string[]} validBridges - The valid bridge lines found.
|
|
| 133 | + */
|
|
| 134 | +/**
|
|
| 135 | + * Validate the given bridge lines.
|
|
| 136 | + *
|
|
| 137 | + * @param {string} bridgeLines - The bridge lines to validate, separated by
|
|
| 138 | + * newlines.
|
|
| 139 | + *
|
|
| 140 | + * @returns {BridgeValidationResult}
|
|
| 141 | + */
|
|
| 142 | +export function validateBridgeLines(bridgeLines) {
|
|
| 143 | + let empty = true;
|
|
| 144 | + const errorLines = [];
|
|
| 145 | + const validBridges = [];
|
|
| 146 | + for (const [index, bridge] of splitBridgeLines(bridgeLines).entries()) {
|
|
| 147 | + if (!bridge) {
|
|
| 148 | + // Empty line.
|
|
| 149 | + continue;
|
|
| 150 | + }
|
|
| 151 | + empty = false;
|
|
| 152 | + try {
|
|
| 153 | + // TODO: Have a more comprehensive validation parser.
|
|
| 154 | + lazy.TorParsers.parseBridgeLine(bridge);
|
|
| 155 | + } catch {
|
|
| 156 | + errorLines.push(index + 1);
|
|
| 157 | + continue;
|
|
| 158 | + }
|
|
| 159 | + validBridges.push(bridge);
|
|
| 160 | + }
|
|
| 161 | + return { empty, errorLines, validBridges };
|
|
| 126 | 162 | }
|
| 127 | 163 | |
| 128 | 164 | /**
|
| ... | ... | @@ -269,7 +305,8 @@ class TorSettingsImpl { |
| 269 | 305 | if (Array.isArray(val)) {
|
| 270 | 306 | return [...val];
|
| 271 | 307 | }
|
| 272 | - return parseBridgeStrings(val);
|
|
| 308 | + // Split the bridge strings, discarding empty.
|
|
| 309 | + return splitBridgeLines(val).filter(val => val);
|
|
| 273 | 310 | },
|
| 274 | 311 | copy: val => [...val],
|
| 275 | 312 | equal: (val1, val2) => this.#arrayEqual(val1, val2),
|
| ... | ... | @@ -139,10 +139,6 @@ const Loader = { |
| 139 | 139 | solveTheCaptcha: "Solve the CAPTCHA to request a bridge.",
|
| 140 | 140 | captchaTextboxPlaceholder: "Enter the characters from the image",
|
| 141 | 141 | incorrectCaptcha: "The solution is not correct. Please try again.",
|
| 142 | - // Provide bridge dialog
|
|
| 143 | - provideBridgeDescription:
|
|
| 144 | - "Add a bridge provided by a trusted organization or someone you know. If you don’t have a bridge, you can request one from the Tor Project. %S",
|
|
| 145 | - provideBridgePlaceholder: "type address:port (one per line)",
|
|
| 146 | 142 | // Connection settings dialog
|
| 147 | 143 | connectionSettingsDialogTitle: "Connection Settings",
|
| 148 | 144 | connectionSettingsDialogHeader:
|
| ... | ... | @@ -75,10 +75,6 @@ settings.solveTheCaptcha=Solve the CAPTCHA to request a bridge. |
| 75 | 75 | settings.captchaTextboxPlaceholder=Enter the characters from the image
|
| 76 | 76 | settings.incorrectCaptcha=The solution is not correct. Please try again.
|
| 77 | 77 | |
| 78 | -# Translation note: %S is a Learn more link.
|
|
| 79 | -settings.provideBridgeDescription=Add a bridge provided by a trusted organization or someone you know. If you don’t have a bridge, you can request one from the Tor Project. %S
|
|
| 80 | -settings.provideBridgePlaceholder=type address:port (one per line)
|
|
| 81 | - |
|
| 82 | 78 | # Connection settings dialog
|
| 83 | 79 | settings.connectionSettingsDialogTitle=Connection Settings
|
| 84 | 80 | settings.connectionSettingsDialogHeader=Configure how Tor Browser connects to the Internet
|
| ... | ... | @@ -126,3 +122,6 @@ settings.bridgeAddManually=Add a Bridge Manually… |
| 126 | 122 | |
| 127 | 123 | # Provide bridge dialog
|
| 128 | 124 | settings.provideBridgeTitleAdd=Add a Bridge Manually
|
| 125 | +# Translation note: %S is a Learn more link.
|
|
| 126 | +settings.provideBridgeDescription=Add a bridge provided by a trusted organization or someone you know. If you don’t have a bridge, you can request one from the Tor Project. %S
|
|
| 127 | +settings.provideBridgePlaceholder=type address:port (one per line) |