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 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 41913: Add basic validation to user provided bridge dialog, and a confirmation step.
- - - - - 6fe742ee by Henry Wilkes at 2024-01-29T18:34:30+00:00 fixup! Tor Browser strings
Bug 41913: Add strings for user provided bridge dialog.
- - - - - 7abc2cc1 by Henry Wilkes at 2024-01-29T18:34:30+00:00 fixup! Bug 40597: Implement TorSettings module
Bug 41913: Add public validateBridgeLines method.
- - - - - fd4565d1 by Henry Wilkes at 2024-01-29T18:34:30+00:00 fixup! Add TorStrings module for localization
Bug 41913: Remove old strings for user provided bridge addresses.
- - - - -
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:
===================================== browser/components/preferences/preferences.xhtml ===================================== @@ -70,6 +70,7 @@ <script type="module" src="chrome://global/content/elements/moz-support-link.mjs"/> <script src="chrome://browser/content/migration/migration-wizard.mjs" type="module"></script> <script type="module" src="chrome://global/content/elements/moz-toggle.mjs"/> + <script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js"/> </head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
===================================== browser/components/torpreferences/content/bridgemoji/BridgeEmoji.js ===================================== @@ -0,0 +1,199 @@ +"use strict"; + +{ + /** + * Element to display a single bridge emoji, with a localized name. + */ + class BridgeEmoji extends HTMLElement { + static #activeInstances = new Set(); + static #observer(subject, topic, data) { + if (topic === "intl:app-locales-changed") { + BridgeEmoji.#updateEmojiLangCode(); + } + } + + static #addActiveInstance(inst) { + if (this.#activeInstances.size === 0) { + Services.obs.addObserver(this.#observer, "intl:app-locales-changed"); + this.#updateEmojiLangCode(); + } + this.#activeInstances.add(inst); + } + + static #removeActiveInstance(inst) { + this.#activeInstances.delete(inst); + if (this.#activeInstances.size === 0) { + Services.obs.removeObserver(this.#observer, "intl:app-locales-changed"); + } + } + + /** + * The language code for emoji annotations. + * + * null if unset. + * + * @type {string?} + */ + static #emojiLangCode = null; + /** + * A promise that resolves to two JSON structures for bridge-emojis.json and + * annotations.json, respectively. + * + * @type {Promise} + */ + static #emojiPromise = Promise.all([ + fetch( + "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json" + ).then(response => response.json()), + fetch( + "chrome://browser/content/torpreferences/bridgemoji/annotations.json" + ).then(response => response.json()), + ]); + + static #unknownStringPromise = null; + + /** + * Update #emojiLangCode. + */ + static async #updateEmojiLangCode() { + let langCode; + const emojiAnnotations = (await BridgeEmoji.#emojiPromise)[1]; + // Find the first desired locale we have annotations for. + // Add "en" as a fallback. + for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) { + langCode = bcp47; + if (langCode in emojiAnnotations) { + break; + } + // Remove everything after the dash, if there is one. + langCode = bcp47.replace(/-.*/, ""); + if (langCode in emojiAnnotations) { + break; + } + } + if (langCode !== this.#emojiLangCode) { + this.#emojiLangCode = langCode; + this.#unknownStringPromise = document.l10n.formatValue( + "tor-bridges-emoji-unknown" + ); + for (const inst of this.#activeInstances) { + inst.update(); + } + } + } + + /** + * Update the bridge emoji to show their corresponding emoji with an + * annotation that matches the current locale. + */ + async update() { + if (!this.#active) { + return; + } + + if (!BridgeEmoji.#emojiLangCode) { + // No lang code yet, wait until it is updated. + return; + } + + const doc = this.ownerDocument; + const [unknownString, [emojiList, emojiAnnotations]] = await Promise.all([ + BridgeEmoji.#unknownStringPromise, + BridgeEmoji.#emojiPromise, + ]); + + const emoji = emojiList[this.#index]; + let emojiName; + if (!emoji) { + // Unexpected. + this.#img.removeAttribute("src"); + } else { + const cp = emoji.codePointAt(0).toString(16); + this.#img.setAttribute( + "src", + `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg` + ); + emojiName = emojiAnnotations[BridgeEmoji.#emojiLangCode][cp]; + } + if (!emojiName) { + doc.defaultView.console.error(`No emoji for index ${this.#index}`); + emojiName = unknownString; + } + doc.l10n.setAttributes(this, "tor-bridges-emoji-cell", { + emojiName, + }); + } + + /** + * The index for this bridge emoji. + * + * @type {integer?} + */ + #index = null; + /** + * Whether we are active (i.e. in the DOM). + * + * @type {boolean} + */ + #active = false; + /** + * The image element. + * + * @type {HTMLImgElement?} + */ + #img = null; + + constructor(index) { + super(); + this.#index = index; + } + + connectedCallback() { + if (!this.#img) { + this.#img = this.ownerDocument.createElement("img"); + this.#img.classList.add("tor-bridges-emoji-icon"); + this.#img.setAttribute("alt", ""); + this.appendChild(this.#img); + } + + this.#active = true; + BridgeEmoji.#addActiveInstance(this); + this.update(); + } + + disconnectedCallback() { + this.#active = false; + BridgeEmoji.#removeActiveInstance(this); + } + + /** + * Create four bridge emojis for the given address. + * + * @param {string} bridgeLine - The bridge address. + * + * @returns {BridgeEmoji[4]} - The bridge emoji elements. + */ + static createForAddress(bridgeLine) { + // JS uses UTF-16. While most of these emojis are surrogate pairs, a few + // ones fit one UTF-16 character. So we could not use neither indices, + // nor substr, nor some function to split the string. + // FNV-1a implementation that is compatible with other languages + const prime = 0x01000193; + const offset = 0x811c9dc5; + let hash = offset; + const encoder = new TextEncoder(); + for (const byte of encoder.encode(bridgeLine)) { + hash = Math.imul(hash ^ byte, prime); + } + + return [ + ((hash & 0x7f000000) >> 24) | (hash < 0 ? 0x80 : 0), + (hash & 0x00ff0000) >> 16, + (hash & 0x0000ff00) >> 8, + hash & 0x000000ff, + ].map(index => new BridgeEmoji(index)); + } + } + + customElements.define("tor-bridge-emoji", BridgeEmoji); +}
===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -299,12 +299,10 @@ const gBridgeGrid = {
this._active = true;
- Services.obs.addObserver(this, "intl:app-locales-changed"); Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
this._grid.classList.add("grid-active");
- this._updateEmojiLangCode(); this._updateConnectedBridge(); },
@@ -322,7 +320,6 @@ const gBridgeGrid = {
this._grid.classList.remove("grid-active");
- Services.obs.removeObserver(this, "intl:app-locales-changed"); Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged); },
@@ -337,9 +334,6 @@ const gBridgeGrid = { this._updateRows(); } break; - case "intl:app-locales-changed": - this._updateEmojiLangCode(); - break; case TorProviderTopics.BridgeChanged: this._updateConnectedBridge(); break; @@ -573,97 +567,6 @@ const gBridgeGrid = { } },
- /** - * The language code for emoji annotations. - * - * null if unset. - * - * @type {string?} - */ - _emojiLangCode: null, - /** - * A promise that resolves to two JSON structures for bridge-emojis.json and - * annotations.json, respectively. - * - * @type {Promise} - */ - _emojiPromise: Promise.all([ - fetch( - "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json" - ).then(response => response.json()), - fetch( - "chrome://browser/content/torpreferences/bridgemoji/annotations.json" - ).then(response => response.json()), - ]), - - /** - * Update _emojiLangCode. - */ - async _updateEmojiLangCode() { - let langCode; - const emojiAnnotations = (await this._emojiPromise)[1]; - // Find the first desired locale we have annotations for. - // Add "en" as a fallback. - for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) { - langCode = bcp47; - if (langCode in emojiAnnotations) { - break; - } - // Remove everything after the dash, if there is one. - langCode = bcp47.replace(/-.*/, ""); - if (langCode in emojiAnnotations) { - break; - } - } - if (langCode !== this._emojiLangCode) { - this._emojiLangCode = langCode; - for (const row of this._rows) { - this._updateRowEmojis(row); - } - } - }, - - /** - * Update the bridge emojis to show their corresponding emoji with an - * annotation that matches the current locale. - * - * @param {BridgeGridRow} row - The row to update the emojis of. - */ - async _updateRowEmojis(row) { - if (!this._emojiLangCode) { - // No lang code yet, wait until it is updated. - return; - } - - const [emojiList, emojiAnnotations] = await this._emojiPromise; - const unknownString = await document.l10n.formatValue( - "tor-bridges-emoji-unknown" - ); - - for (const { cell, img, index } of row.emojis) { - const emoji = emojiList[index]; - let emojiName; - if (!emoji) { - // Unexpected. - img.removeAttribute("src"); - } else { - const cp = emoji.codePointAt(0).toString(16); - img.setAttribute( - "src", - `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg` - ); - emojiName = emojiAnnotations[this._emojiLangCode][cp]; - } - if (!emojiName) { - console.error(`No emoji for index ${index}`); - emojiName = unknownString; - } - document.l10n.setAttributes(cell, "tor-bridges-emoji-cell", { - emojiName, - }); - } - }, - /** * Create a new row for the grid. * @@ -688,23 +591,14 @@ const gBridgeGrid = { };
const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block"); - row.emojis = makeBridgeId(bridgeLine).map(index => { - const cell = document.createElement("span"); - // Each emoji is its own cell, we rely on the fact that makeBridgeId - // always returns four indices. + const BridgeEmoji = customElements.get("tor-bridge-emoji"); + for (const cell of BridgeEmoji.createForAddress(bridgeLine)) { + // Each emoji is its own cell, we rely on the fact that createForAddress + // always returns four elements. cell.setAttribute("role", "gridcell"); cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell"); - - const img = document.createElement("img"); - img.classList.add("tor-bridges-emoji-icon"); - // Accessible name will be set on the cell itself. - img.setAttribute("alt", ""); - - cell.appendChild(img); - emojiBlock.appendChild(cell); - // Image and text is set in _updateRowEmojis. - return { cell, img, index }; - }); + emojiBlock.append(cell); + }
for (const [columnIndex, element] of row.element .querySelectorAll(".tor-bridges-grid-cell") @@ -735,7 +629,6 @@ const gBridgeGrid = { this._initRowMenu(row);
this._updateRowStatus(row); - this._updateRowEmojis(row); return row; },
@@ -1870,13 +1763,13 @@ const gBridgeSettings = { "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml", { mode }, result => { - if (!result.bridgeStrings) { + if (!result.bridges?.length) { return null; } return setTorSettings(() => { TorSettings.bridges.enabled = true; TorSettings.bridges.source = TorBridgeSource.UserProvided; - TorSettings.bridges.bridge_strings = result.bridgeStrings; + TorSettings.bridges.bridge_strings = result.bridges; }); } ); @@ -2292,32 +2185,3 @@ const gConnectionPane = (function () { }; return retval; })(); /* gConnectionPane */ - -/** - * Convert the given bridgeString into an array of emoji indices between 0 and - * 255. - * - * @param {string} bridgeString - The bridge string. - * - * @returns {integer[]} - A list of emoji indices between 0 and 255. - */ -function makeBridgeId(bridgeString) { - // JS uses UTF-16. While most of these emojis are surrogate pairs, a few - // ones fit one UTF-16 character. So we could not use neither indices, - // nor substr, nor some function to split the string. - // FNV-1a implementation that is compatible with other languages - const prime = 0x01000193; - const offset = 0x811c9dc5; - let hash = offset; - const encoder = new TextEncoder(); - for (const byte of encoder.encode(bridgeString)) { - hash = Math.imul(hash ^ byte, prime); - } - - return [ - ((hash & 0x7f000000) >> 24) | (hash < 0 ? 0x80 : 0), - (hash & 0x00ff0000) >> 16, - (hash & 0x0000ff00) >> 8, - hash & 0x000000ff, - ]; -}
===================================== browser/components/torpreferences/content/connectionPane.xhtml ===================================== @@ -218,6 +218,7 @@ </html:div> <html:div id="tor-bridges-grid-display" + class="tor-bridges-grid" role="grid" aria-labelledby="tor-bridges-current-heading" ></html:div>
===================================== browser/components/torpreferences/content/provideBridgeDialog.js ===================================== @@ -4,14 +4,17 @@ const { TorStrings } = ChromeUtils.importESModule( "resource://gre/modules/TorStrings.sys.mjs" );
-const { TorSettings, TorBridgeSource } = ChromeUtils.importESModule( - "resource://gre/modules/TorSettings.sys.mjs" -); +const { TorSettings, TorBridgeSource, validateBridgeLines } = + ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule( "resource://gre/modules/TorConnect.sys.mjs" );
+const { TorParsers } = ChromeUtils.importESModule( + "resource://gre/modules/TorParsers.sys.mjs" +); + const gProvideBridgeDialog = { init() { this._result = window.arguments[0]; @@ -33,72 +36,264 @@ const gProvideBridgeDialog = {
document.l10n.setAttributes(document.documentElement, titleId);
- const learnMore = document.createXULElement("label"); - learnMore.className = "learnMore text-link"; - learnMore.setAttribute("is", "text-link"); - learnMore.setAttribute("value", TorStrings.settings.learnMore); - learnMore.addEventListener("click", () => { - window.top.openTrustedLinkIn( - TorStrings.settings.learnMoreBridgesURL, - "tab" - ); - }); - - const pieces = TorStrings.settings.provideBridgeDescription.split("%S"); - document - .getElementById("torPreferences-provideBridge-description") - .replaceChildren(pieces[0], learnMore, pieces[1] || ""); + document.l10n.setAttributes( + document.getElementById("user-provide-bridge-textarea-label"), + // TODO change string when we can also accept Lox share codes. + "user-provide-bridge-dialog-textarea-addresses-label" + );
- this._textarea = document.getElementById( - "torPreferences-provideBridge-textarea" + this._dialog = document.getElementById("user-provide-bridge-dialog"); + this._acceptButton = this._dialog.getButton("accept"); + this._textarea = document.getElementById("user-provide-bridge-textarea"); + this._errorEl = document.getElementById( + "user-provide-bridge-error-message" + ); + this._resultDescription = document.getElementById( + "user-provide-result-description" + ); + this._bridgeGrid = document.getElementById( + "user-provide-bridge-grid-display" ); - this._textarea.setAttribute( - "placeholder", - TorStrings.settings.provideBridgePlaceholder + this._rowTemplate = document.getElementById( + "user-provide-bridge-row-template" );
- this._textarea.addEventListener("input", () => this.onValueChange()); - if (TorSettings.bridges.source == TorBridgeSource.UserProvided) { - this._textarea.value = TorSettings.bridges.bridge_strings.join("\n"); + if (mode === "edit") { + // Only expected if the bridge source is UseProvided, but verify to be + // sure. + if (TorSettings.bridges.source == TorBridgeSource.UserProvided) { + this._textarea.value = TorSettings.bridges.bridge_strings.join("\n"); + } + } else { + // Set placeholder if not editing. + document.l10n.setAttributes( + this._textarea, + // TODO: change string when we can also accept Lox share codes. + "user-provide-bridge-dialog-textarea-addresses" + ); }
- const dialog = document.getElementById( - "torPreferences-provideBridge-dialog" - ); - dialog.addEventListener("dialogaccept", e => { - this._result.accepted = true; - }); + this._textarea.addEventListener("input", () => this.onValueChange());
- this._acceptButton = dialog.getButton("accept"); + this._dialog.addEventListener("dialogaccept", event => + this.onDialogAccept(event) + );
Services.obs.addObserver(this, TorConnectTopics.StateChange);
- this.onValueChange(); - this.onAcceptStateChange(); + this.setPage("entry"); + this.checkValue(); },
uninit() { Services.obs.removeObserver(this, TorConnectTopics.StateChange); },
+ /** + * Set the page to display. + * + * @param {string} page - The page to show. + */ + setPage(page) { + this._page = page; + this._dialog.classList.toggle("show-entry-page", page === "entry"); + this._dialog.classList.toggle("show-result-page", page === "result"); + if (page === "entry") { + this._textarea.focus(); + } else { + // Move focus to the xul:window element. + // In particular, we do not want to keep the focus on the (same) accept + // button (with now different text). + document.documentElement.focus(); + } + + this.updateAcceptDisabled(); + this.onAcceptStateChange(); + }, + + /** + * Callback for whenever the input value changes. + */ onValueChange() { - // TODO: Do some proper value parsing and error reporting. See - // tor-browser#40552. - const value = this._textarea.value.trim(); - this._acceptButton.disabled = !value; - this._result.bridgeStrings = value; + this.updateAcceptDisabled(); + // Reset errors whenever the value changes. + this.updateError(null); },
+ /** + * Callback for whenever the accept button may need to change. + */ onAcceptStateChange() { - const connect = TorConnect.canBeginBootstrap; - this._result.connect = connect; - - this._acceptButton.setAttribute( - "label", - connect - ? TorStrings.settings.bridgeButtonConnect - : TorStrings.settings.bridgeButtonAccept + if (this._page === "entry") { + document.l10n.setAttributes( + this._acceptButton, + "user-provide-bridge-dialog-next-button" + ); + this._result.connect = false; + } else { + this._acceptButton.removeAttribute("data-l10n-id"); + const connect = TorConnect.canBeginBootstrap; + this._result.connect = connect; + + this._acceptButton.setAttribute( + "label", + connect + ? TorStrings.settings.bridgeButtonConnect + : TorStrings.settings.bridgeButtonAccept + ); + } + }, + + /** + * Callback for whenever the accept button's might need to be disabled. + */ + updateAcceptDisabled() { + this._acceptButton.disabled = + this._page === "entry" && validateBridgeLines(this._textarea.value).empty; + }, + + /** + * Callback for when the accept button is pressed. + * + * @param {Event} event - The dialogaccept event. + */ + onDialogAccept(event) { + if (this._page === "result") { + this._result.accepted = true; + // Continue to close the dialog. + return; + } + // Prevent closing the dialog. + event.preventDefault(); + + const bridges = this.checkValue(); + if (!bridges.length) { + // Not valid + return; + } + this._result.bridges = bridges; + this.updateResult(); + this.setPage("result"); + }, + + /** + * The current timeout for updating the error. + * + * @type {integer?} + */ + _updateErrorTimeout: null, + + /** + * Update the displayed error. + * + * @param {object?} error - The error to show, or null if no error should be + * shown. Should include the "type" property. + */ + updateError(error) { + // First clear the existing error. + if (this._updateErrorTimeout !== null) { + clearTimeout(this._updateErrorTimeout); + } + this._updateErrorTimeout = null; + this._errorEl.removeAttribute("data-l10n-id"); + this._errorEl.textContent = ""; + if (error) { + this._textarea.setAttribute("aria-invalid", "true"); + } else { + this._textarea.removeAttribute("aria-invalid"); + } + this._textarea.classList.toggle("invalid-input", !!error); + this._errorEl.classList.toggle("show-error", !!error); + + if (!error) { + return; + } + + let errorId; + let errorArgs; + switch (error.type) { + case "invalid-address": + errorId = "user-provide-bridge-dialog-address-error"; + errorArgs = { line: error.line }; + break; + } + + // Wait a small amount of time to actually set the textContent. Otherwise + // the screen reader (tested with Orca) may not pick up on the change in + // text. + this._updateErrorTimeout = setTimeout(() => { + document.l10n.setAttributes(this._errorEl, errorId, errorArgs); + }, 500); + }, + + /** + * Check the current value in the textarea. + * + * @returns {string[]} - The bridge addresses, if the entry is valid. + */ + checkValue() { + let bridges = []; + let error = null; + const validation = validateBridgeLines(this._textarea.value); + if (!validation.empty) { + // If empty, we just disable the button, rather than show an error. + if (validation.errorLines.length) { + // Report first error. + error = { + type: "invalid-address", + line: validation.errorLines[0], + }; + } else { + bridges = validation.validBridges; + } + } + this.updateError(error); + return bridges; + }, + + /** + * Update the shown result on the last page. + */ + updateResult() { + document.l10n.setAttributes( + this._resultDescription, + // TODO: Use a different id when added through Lox invite. + "user-provide-bridge-dialog-result-addresses" ); + + this._bridgeGrid.replaceChildren(); + + for (const bridgeLine of this._result.bridges) { + let details; + try { + details = TorParsers.parseBridgeLine(bridgeLine); + } catch (e) { + console.error(`Detected invalid bridge line: ${bridgeLine}`, e); + } + + const rowEl = this._rowTemplate.content.children[0].cloneNode(true); + + const emojiBlock = rowEl.querySelector(".tor-bridges-emojis-block"); + const BridgeEmoji = customElements.get("tor-bridge-emoji"); + for (const cell of BridgeEmoji.createForAddress(bridgeLine)) { + // Each emoji is its own cell, we rely on the fact that createForAddress + // always returns four elements. + cell.setAttribute("role", "gridcell"); + cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell"); + emojiBlock.append(cell); + } + + // TODO: properly handle "vanilla" bridges? + document.l10n.setAttributes( + rowEl.querySelector(".tor-bridges-type-cell"), + "tor-bridges-type-prefix", + { type: details?.transport ?? "vanilla" } + ); + + rowEl.querySelector(".tor-bridges-address-cell").textContent = bridgeLine; + + this._bridgeGrid.append(rowEl); + } },
observe(subject, topic, data) {
===================================== browser/components/torpreferences/content/provideBridgeDialog.xhtml ===================================== @@ -8,22 +8,77 @@ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
- <dialog id="torPreferences-provideBridge-dialog" buttons="accept,cancel"> + <dialog + id="user-provide-bridge-dialog" + buttons="accept,cancel" + class="show-entry-page" + > <linkset> <html:link rel="localization" href="browser/tor-browser.ftl" /> </linkset>
+ <script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js" /> <script src="chrome://browser/content/torpreferences/provideBridgeDialog.js" />
- <description> - <html:div id="torPreferences-provideBridge-description" - >​<br />​</html:div - > - </description> - <html:textarea - id="torPreferences-provideBridge-textarea" - multiline="true" - rows="3" - /> + <html:div id="user-provide-bridge-entry-page"> + <description id="user-provide-bridge-description"> + <html:span + class="tail-with-learn-more" + data-l10n-id="user-provide-bridge-dialog-description" + ></html:span> + <label + is="text-link" + class="learnMore text-link" + href="about:manual#bridges" + useoriginprincipal="true" + data-l10n-id="user-provide-bridge-dialog-learn-more" + /> + </description> + <html:label + id="user-provide-bridge-textarea-label" + for="user-provide-bridge-textarea" + ></html:label> + <html:textarea + id="user-provide-bridge-textarea" + multiline="true" + rows="3" + aria-describedby="user-provide-bridge-description" + aria-errormessage="user-provide-bridge-error-message" + /> + <html:div id="user-provide-bridge-message-area"> + <html:span + id="user-provide-bridge-error-message" + aria-live="assertive" + ></html:span> + </html:div> + </html:div> + <html:div id="user-provide-bridge-result-page"> + <description id="user-provide-result-description" /> + <!-- NOTE: Unlike #tor-bridge-grid-display, this element is not + - interactive, and not a tab-stop. So we use the "table" role rather + - than "grid". + - NOTE: Using a <html:table> would not allow us the same structural + - freedom, so we use a generic div and add the semantics manually. --> + <html:div + id="user-provide-bridge-grid-display" + class="tor-bridges-grid" + role="table" + ></html:div> + <html:template id="user-provide-bridge-row-template"> + <html:div class="tor-bridges-grid-row" role="row"> + <html:span + class="tor-bridges-type-cell tor-bridges-grid-cell" + role="gridcell" + ></html:span> + <html:span class="tor-bridges-emojis-block" role="none"></html:span> + <html:span class="tor-bridges-grid-end-block" role="none"> + <html:span + class="tor-bridges-address-cell tor-bridges-grid-cell" + role="gridcell" + ></html:span> + </html:span> + </html:div> + </html:template> + </html:div> </dialog> </window>
===================================== browser/components/torpreferences/content/torPreferences.css ===================================== @@ -270,11 +270,14 @@ grid-area: description; }
-#tor-bridges-grid-display { +.tor-bridges-grid { display: grid; grid-template-columns: max-content repeat(4, max-content) 1fr; --tor-bridges-grid-column-gap: 8px; --tor-bridges-grid-column-short-gap: 4px; + /* For #tor-bridges-grid-display we want each grid item to have the same + * height so that their focus outlines match. */ + align-items: stretch; }
#tor-bridges-grid-display:not(.grid-active) { @@ -283,11 +286,12 @@
.tor-bridges-grid-row { /* We want each row to act as a row of three items in the - * #tor-bridges-grid-display grid layout. + * .tor-bridges-grid grid layout. * We also want a 16px spacing between rows, and 8px spacing between columns, - * which are outside the .tor-bridges-grid-cell's border area. So that - * clicking these gaps will not focus any item, and their focus outlines do - * not overlap. + * which are outside the .tor-bridges-grid-cell's border area. + * + * For #tor-bridges-grid-display this should ensure that clicking these gaps + * will not focus any item, and their focus outlines do not overlap. * Moreover, we also want each row to show its .tor-bridges-options-cell when * the .tor-bridges-grid-row has :hover. * @@ -311,7 +315,8 @@ padding-block: 8px; }
-.tor-bridges-grid-cell:focus-visible { +#tor-bridges-grid-display .tor-bridges-grid-cell:focus-visible { + /* #tor-bridges-grid-display has focus management for its cells. */ outline: var(--in-content-focus-outline); outline-offset: var(--in-content-focus-outline-offset); } @@ -662,8 +667,77 @@ groupbox#torPreferences-bridges-group textarea { }
/* Provide bridge dialog */ -#torPreferences-provideBridge-textarea { - margin-top: 16px; + +#user-provide-bridge-dialog:not(.show-entry-page) #user-provide-bridge-entry-page { + display: none; +} + +#user-provide-bridge-dialog:not(.show-result-page) #user-provide-bridge-result-page { + display: none; +} + +#user-provide-bridge-entry-page { + flex: 1 0 auto; + display: flex; + flex-direction: column; +} + +#user-provide-bridge-description { + flex: 0 0 auto; +} + +#user-provide-bridge-textarea-label { + margin-block: 16px 6px; + flex: 0 0 auto; + align-self: start; +} + +#user-provide-bridge-textarea { + flex: 1 0 auto; + align-self: stretch; + line-height: 1.3; + margin: 0; +} + +#user-provide-bridge-message-area { + flex: 0 0 auto; + margin-block: 8px 12px; + align-self: end; +} + +#user-provide-bridge-message-area::after { + /* Zero width space, to ensure we are always one line high. */ + content: "\200B"; +} + +#user-provide-bridge-textarea.invalid-input { + border-color: var(--in-content-danger-button-background); + outline-color: var(--in-content-danger-button-background); +} + +#user-provide-bridge-error-message { + color: var(--in-content-error-text-color); +} + +#user-provide-bridge-error-message.not(.show-error) { + display: none; +} + +#user-provide-bridge-result-page { + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; +} + +#user-provide-result-description { + flex: 0 0 auto; +} + +#user-provide-bridge-grid-display { + flex: 0 1 auto; + overflow: auto; + margin-block: 8px; }
/* Connection settings dialog */
===================================== browser/components/torpreferences/jar.mn ===================================== @@ -22,6 +22,7 @@ browser.jar: content/browser/torpreferences/connectionPane.xhtml (content/connectionPane.xhtml) content/browser/torpreferences/torPreferences.css (content/torPreferences.css) content/browser/torpreferences/bridge-qr-onion-mask.svg (content/bridge-qr-onion-mask.svg) + content/browser/torpreferences/bridgemoji/BridgeEmoji.js (content/bridgemoji/BridgeEmoji.js) content/browser/torpreferences/bridgemoji/bridge-emojis.json (content/bridgemoji/bridge-emojis.json) content/browser/torpreferences/bridgemoji/annotations.json (content/bridgemoji/annotations.json) 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 = # Used when the user is replacing their existing bridges with new ones. user-provide-bridge-dialog-replace-title = .title = Replace your bridges +# Description shown when adding new bridges, replacing existing bridges, or editing existing bridges. +user-provide-bridge-dialog-description = Use bridges provided by a trusted organisation or someone you know. +# "Learn more" link shown in the "Add new bridges"/"Replace your bridges" dialog. +user-provide-bridge-dialog-learn-more = Learn more +# Short accessible name for the bridge addresses text area. +user-provide-bridge-dialog-textarea-addresses-label = Bridge addresses +# Placeholder shown when adding new bridge addresses. +user-provide-bridge-dialog-textarea-addresses = + .placeholder = Paste your bridge addresses here +# Error shown when one of the address lines is invalid. +# $line (Number) - The line number for the invalid address. +user-provide-bridge-dialog-address-error = Incorrectly formatted bridge address on line { $line }. + +user-provide-bridge-dialog-result-addresses = The following bridges were entered by you. +user-provide-bridge-dialog-next-button = + .label = Next
===================================== toolkit/modules/TorSettings.sys.mjs ===================================== @@ -9,6 +9,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs", Lox: "resource://gre/modules/Lox.sys.mjs", + TorParsers: "resource://gre/modules/TorParsers.sys.mjs", });
ChromeUtils.defineLazyGetter(lazy, "logger", () => { @@ -103,26 +104,61 @@ export const TorProxyType = Object.freeze({ * Split a blob of bridge lines into an array with single lines. * Lines are delimited by \r\n or \n and each bridge string can also optionally * have 'bridge' at the beginning. - * We split the text by \r\n, we trim the lines, remove the bridge prefix and - * filter out any remaiing empty item. + * We split the text by \r\n, we trim the lines, remove the bridge prefix. * - * @param {string} aBridgeStrings The text with the lines + * @param {string} bridgeLines The text with the lines * @returns {string[]} An array where each bridge line is an item */ -function parseBridgeStrings(aBridgeStrings) { - // replace carriage returns ('\r') with new lines ('\n') - aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n"); - // then replace contiguous new lines ('\n') with a single one - aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n"); - +function splitBridgeLines(bridgeLines) { // Split on the newline and for each bridge string: trim, remove starting // 'bridge' string. - // Finally, discard entries that are empty strings; empty strings could occur - // if we receive a new line containing only whitespace. - const splitStrings = aBridgeStrings.split("\n"); - return splitStrings - .map(val => val.trim().replace(/^bridge\s+/i, "")) - .filter(bridgeString => bridgeString !== ""); + // Replace whitespace with standard " ". + // NOTE: We only remove the bridge string part if it is followed by a + // non-whitespace. + return bridgeLines.split(/\r?\n/).map(val => + val + .trim() + .replace(/^bridge\s+(\S)/i, "$1") + .replace(/\s+/, " ") + ); +} + +/** + * @typedef {Object} BridgeValidationResult + * + * @property {integer[]} errorLines - The lines that contain errors. Counting + * from 1. + * @property {boolean} empty - Whether the given string contains no bridges. + * @property {string[]} validBridges - The valid bridge lines found. + */ +/** + * Validate the given bridge lines. + * + * @param {string} bridgeLines - The bridge lines to validate, separated by + * newlines. + * + * @returns {BridgeValidationResult} + */ +export function validateBridgeLines(bridgeLines) { + let empty = true; + const errorLines = []; + const validBridges = []; + for (const [index, bridge] of splitBridgeLines(bridgeLines).entries()) { + if (!bridge) { + // Empty line. + continue; + } + empty = false; + try { + // TODO: Have a more comprehensive validation parser. + lazy.TorParsers.parseBridgeLine(bridge); + } catch { + errorLines.push(index + 1); + continue; + } + validBridges.push(bridge); + } + return { empty, errorLines, validBridges }; }
/** @@ -269,7 +305,8 @@ class TorSettingsImpl { if (Array.isArray(val)) { return [...val]; } - return parseBridgeStrings(val); + // Split the bridge strings, discarding empty. + return splitBridgeLines(val).filter(val => val); }, copy: val => [...val], equal: (val1, val2) => this.#arrayEqual(val1, val2),
===================================== toolkit/modules/TorStrings.sys.mjs ===================================== @@ -139,10 +139,6 @@ const Loader = { solveTheCaptcha: "Solve the CAPTCHA to request a bridge.", captchaTextboxPlaceholder: "Enter the characters from the image", incorrectCaptcha: "The solution is not correct. Please try again.", - // Provide bridge dialog - 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", - provideBridgePlaceholder: "type address:port (one per line)", // Connection settings dialog connectionSettingsDialogTitle: "Connection Settings", connectionSettingsDialogHeader:
===================================== toolkit/torbutton/chrome/locale/en-US/settings.properties ===================================== @@ -75,10 +75,6 @@ settings.solveTheCaptcha=Solve the CAPTCHA to request a bridge. settings.captchaTextboxPlaceholder=Enter the characters from the image settings.incorrectCaptcha=The solution is not correct. Please try again.
-# Translation note: %S is a Learn more link. -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 -settings.provideBridgePlaceholder=type address:port (one per line) - # Connection settings dialog settings.connectionSettingsDialogTitle=Connection Settings settings.connectionSettingsDialogHeader=Configure how Tor Browser connects to the Internet @@ -126,3 +122,6 @@ settings.bridgeAddManually=Add a Bridge Manually…
# Provide bridge dialog settings.provideBridgeTitleAdd=Add a Bridge Manually +# Translation note: %S is a Learn more link. +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 +settings.provideBridgePlaceholder=type address:port (one per line)
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/366c81d...
tor-commits@lists.torproject.org