richard pushed to branch tor-browser-115.7.0esr-13.5-1 at The Tor Project / Applications / Tor Browser

Commits:

12 changed files:

Changes:

  • browser/components/preferences/preferences.xhtml
    ... ... @@ -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"
    

  • browser/components/torpreferences/content/bridgemoji/BridgeEmoji.js
    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
    +}

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -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
    -}

  • browser/components/torpreferences/content/connectionPane.xhtml
    ... ... @@ -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>
    

  • browser/components/torpreferences/content/provideBridgeDialog.js
    ... ... @@ -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) {
    

  • browser/components/torpreferences/content/provideBridgeDialog.xhtml
    ... ... @@ -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
    -        >&#8203;<br />&#8203;</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>

  • browser/components/torpreferences/content/torPreferences.css
    ... ... @@ -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 */
    

  • browser/components/torpreferences/jar.mn
    ... ... @@ -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)

  • browser/locales/en-US/browser/tor-browser.ftl
    ... ... @@ -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

  • toolkit/modules/TorSettings.sys.mjs
    ... ... @@ -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),
    

  • toolkit/modules/TorStrings.sys.mjs
    ... ... @@ -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:
    

  • toolkit/torbutton/chrome/locale/en-US/settings.properties
    ... ... @@ -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)