Skip to content
Snippets Groups Projects
Commit ace2c34b authored by henry's avatar henry Committed by Pier Angelo Vendrame
Browse files

fixup! Bug 30237: Add v3 onion services client authentication prompt

Bug 42212: Migrate onion service strings to Fluent.

+ Use "onion site" instead of "onionsite" or "onion service".
+ Use sentence case by default.
+ Changed the prompt accept button from "Done" to "OK", in line with
  other prompts.
+ Re-ordered the saved keys dialog introduction text from "Keys for the
  following onionsite are..." to "The following onion site keys are..."
+ Use bold text for the prompt title, rather than just the onion site
  part.
+ Clear the error message in the saved key dialog whenever the user
  tries to remove a key (again).
+ Other small tidies in the touched areas.
parent 8b57c078
No related branches found
No related tags found
1 merge request!1112Migrate onion site strings to Fluent
......@@ -3,4 +3,4 @@
<image id="tor-clientauth-notification-icon"
class="notification-anchor-icon tor-clientauth-icon"
role="button"
tooltiptext="&torbutton.onionServices.authPrompt.tooltip;"/>
data-l10n-id="onion-site-authentication-urlbar-button"/>
......@@ -3,16 +3,24 @@
<popupnotification id="tor-clientauth-notification" hidden="true">
<popupnotificationcontent orient="vertical">
<description id="tor-clientauth-notification-desc" />
<label id="tor-clientauth-notification-learnmore"
<label
class="text-link popup-notification-learnmore-link"
is="text-link"
href="about:manual#onion-services_onion-service-authentication"
useoriginprincipal="true"/>
useoriginprincipal="true"
data-l10n-id="onion-site-authentication-prompt-learn-more"
/>
<html:div>
<html:input id="tor-clientauth-notification-key" type="password"/>
<html:div id="tor-clientauth-warning"/>
<checkbox id="tor-clientauth-persistkey-checkbox"
label="&torbutton.onionServices.authPrompt.persistCheckboxLabel;"/>
<html:input
id="tor-clientauth-notification-key"
type="password"
data-l10n-id="onion-site-authentication-prompt-key-input"
/>
<html:div id="tor-clientauth-warning"></html:div>
<checkbox
id="tor-clientauth-persistkey-checkbox"
data-l10n-id="onion-site-authentication-prompt-remember-checkbox"
/>
</html:div>
</popupnotificationcontent>
</popupnotification>
......@@ -2,21 +2,34 @@
<groupbox id="torOnionServiceKeys" orient="vertical"
data-category="panePrivacy" hidden="true">
<label><html:h2 id="torOnionServiceKeys-header"/></label>
<label><html:h2
data-l10n-id="onion-site-authentication-preferences-heading"
></html:h2></label>
<hbox>
<description
class="description-deemphasized description-with-side-element"
flex="1"
>
<html:span id="torOnionServiceKeys-overview"
class="tail-with-learn-more"/>
<label id="torOnionServiceKeys-learnMore" class="learnMore text-link"
is="text-link"/>
<html:span
id="torOnionServiceKeys-overview"
class="tail-with-learn-more"
data-l10n-id="onion-site-authentication-preferences-overview"
></html:span>
<label
id="torOnionServiceKeys-learnMore"
class="learnMore text-link"
is="text-link"
href="about:manual#onion-services_onion-service-authentication"
useoriginprincipal="true"
data-l10n-id="onion-site-authentication-preferences-learn-more"
/>
</description>
<vbox align="end">
<button id="torOnionServiceKeys-savedKeys"
is="highlightable-button"
class="accessory-button"/>
<html:button
id="torOnionServiceKeys-savedKeys"
class="accessory-button"
data-l10n-id="onion-site-authentication-preferences-saved-keys-button"
></html:button>
</vbox>
</hbox>
</groupbox>
......@@ -2,69 +2,19 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
TorStrings: "resource://gre/modules/TorStrings.sys.mjs",
});
/* globals gSubDialog */
/*
Onion Services Client Authentication Preferences Code
/* import-globals-from /browser/components/preferences/preferences.js */
Code to handle init and update of onion services authentication section
in about:preferences#privacy
/**
* Onion site preferences.
*/
const OnionServicesAuthPreferences = {
selector: {
groupBox: "#torOnionServiceKeys",
header: "#torOnionServiceKeys-header",
overview: "#torOnionServiceKeys-overview",
learnMore: "#torOnionServiceKeys-learnMore",
savedKeysButton: "#torOnionServiceKeys-savedKeys",
},
var OnionServicesAuthPreferences = {
init() {
// populate XUL with localized strings
this._populateXUL();
},
_populateXUL() {
const groupbox = document.querySelector(this.selector.groupBox);
let elem = groupbox.querySelector(this.selector.header);
elem.textContent = TorStrings.onionServices.authPreferences.header;
elem = groupbox.querySelector(this.selector.overview);
elem.textContent = TorStrings.onionServices.authPreferences.overview;
elem = groupbox.querySelector(this.selector.learnMore);
elem.setAttribute("value", TorStrings.onionServices.learnMore);
elem.setAttribute(
"href",
"about:manual#onion-services_onion-service-authentication"
);
elem.setAttribute("useoriginprincipal", "true");
elem = groupbox.querySelector(this.selector.savedKeysButton);
elem.setAttribute(
"label",
TorStrings.onionServices.authPreferences.savedKeys
);
elem.addEventListener("command", () =>
OnionServicesAuthPreferences.onViewSavedKeys()
);
},
onViewSavedKeys() {
document
.getElementById("torOnionServiceKeys-savedKeys")
.addEventListener("click", () => {
gSubDialog.open(
"chrome://browser/content/onionservices/savedKeysDialog.xhtml"
);
},
}; // OnionServicesAuthPreferences
Object.defineProperty(this, "OnionServicesAuthPreferences", {
value: OnionServicesAuthPreferences,
enumerable: true,
writable: false,
});
},
};
......@@ -56,28 +56,47 @@ var OnionAuthPrompt = {
show(details) {
this._logger.debug(`New Notification: ${this._detailsRepr(details)}`);
// NOTE: PopupNotifications currently requires the accesskey and label to be
// set for all actions, and does not accept fluent IDs in their place.
// Moreover, there doesn't appear to be a simple way to work around this, so
// we have to fetch the strings here before calling the show() method.
// NOTE: We avoid using the async formatMessages because we don't want to
// race against the browser's location changing.
// In principle, we could check that the details.browser.currentURI still
// matches details.uri or use a LocationChange listener. However, we expect
// that PopupNotifications will eventually change to accept fluent IDs, so
// we won't have to use formatMessages here at all.
// Moreover, we do not expect this notification to be common, so this
// shouldn't be too expensive.
// NOTE: Once we call PopupNotifications.show, PopupNotifications should
// take care of listening for changes in locations for us and remove the
// notification.
let [okButtonMsg, cancelButtonMsg] = this._lazy.SyncL10n.formatMessagesSync(
[
"onion-site-authentication-prompt-ok-button",
"onion-site-authentication-prompt-cancel-button",
]
);
// Get an attribute string from a L10nMessage.
// We wrap the return value as a String to prevent the notification from
// throwing (and not showing) if a locale is unexpectedly missing a value.
const msgAttribute = (msg, name) =>
String((msg.attributes ?? []).find(attr => attr.name === name)?.value);
let mainAction = {
label: this.TorStrings.onionServices.authPrompt.done,
accessKey: this.TorStrings.onionServices.authPrompt.doneAccessKey,
label: msgAttribute(okButtonMsg, "label"),
accessKey: msgAttribute(okButtonMsg, "accesskey"),
leaveOpen: true, // Callback is responsible for closing the notification.
callback: this._onDone.bind(this),
callback: () => this._onDone(),
};
let dialogBundle = Services.strings.createBundle(
"chrome://global/locale/dialog.properties"
);
let cancelAccessKey = dialogBundle.GetStringFromName("accesskey-cancel");
if (!cancelAccessKey) {
cancelAccessKey = "c";
} // required by PopupNotifications.show()
// The first secondarybuttoncommand (cancelAction) should be triggered when
// the user presses "Escape".
let cancelAction = {
label: dialogBundle.GetStringFromName("button-cancel"),
accessKey: cancelAccessKey,
callback: this._onCancel.bind(this),
label: msgAttribute(cancelButtonMsg, "label"),
accessKey: msgAttribute(cancelButtonMsg, "accesskey"),
callback: () => this._onCancel(),
};
let options = {
......@@ -134,22 +153,17 @@ var OnionAuthPrompt = {
this._keyInput.value = "";
this._persistCheckbox.checked = false;
// Handle replacement of the onion name within the localized
// string ourselves so we can show the onion name as bold text.
// We do this by splitting the localized string and creating
// several HTML <span> elements.
const fmtString = this.TorStrings.onionServices.authPrompt.description;
const [prefix, suffix] = fmtString.split("%S");
const domainEl = document.createElement("span");
domainEl.id = "tor-clientauth-notification-onionname";
domainEl.textContent = TorUIUtils.shortenOnionAddress(
document.l10n.setAttributes(
this._descriptionEl,
"onion-site-authentication-prompt-description",
{
onionsite: TorUIUtils.shortenOnionAddress(
this._shownDetails?.onionHost ?? ""
),
}
);
this._descriptionEl.replaceChildren(prefix, domainEl, suffix);
this._showWarning(undefined);
this._showWarning(null);
},
/**
......@@ -187,12 +201,13 @@ var OnionAuthPrompt = {
);
// Grab the details before they might change as we await.
const { browser, onionServiceId, notification } = this._shownDetails;
const details = this._shownDetails;
const { browser, onionServiceId, notification } = details;
const isPermanent = this._persistCheckbox.checked;
const base64key = this._keyToBase64(this._keyInput.value);
if (!base64key) {
this._showWarning(this.TorStrings.onionServices.authPrompt.invalidKey);
this._showWarning("onion-site-authentication-prompt-invalid-key");
return;
}
......@@ -200,12 +215,11 @@ var OnionAuthPrompt = {
const provider = await this._lazy.TorProviderBuilder.build();
await provider.onionAuthAdd(onionServiceId, base64key, isPermanent);
} catch (e) {
if (e.torMessage) {
this._showWarning(e.torMessage);
} else {
this._logger.error(`Failed to set key for ${onionServiceId}`, e);
if (details === this._shownDetails) {
// Notification has not been replaced.
this._showWarning(
this.TorStrings.onionServices.authPrompt.failedToSetKey
"onion-site-authentication-prompt-setting-key-failed"
);
}
return;
......@@ -245,16 +259,19 @@ var OnionAuthPrompt = {
/**
* Show a warning message to the user or clear the warning.
*
* @param {string?} warningMessage - The message to show, or undefined to
* clear the current message.
* @param {?string} warningMessageId - The l10n ID for the message to show, or
* null to clear the current message.
*/
_showWarning(warningMessage) {
this._logger.debug(`Showing warning: ${warningMessage}`);
if (warningMessage) {
this._warningEl.textContent = warningMessage;
_showWarning(warningMessageId) {
this._logger.debug(`Showing warning: ${warningMessageId}`);
if (warningMessageId) {
document.l10n.setAttributes(this._warningEl, warningMessageId);
this._warningEl.removeAttribute("hidden");
this._keyInput.classList.add("invalid");
} else {
// Clean up.
this._warningEl.removeAttribute("data-l10n-id");
this._warningEl.textContent = "";
this._warningEl.setAttribute("hidden", "true");
this._keyInput.classList.remove("invalid");
}
......@@ -264,7 +281,7 @@ var OnionAuthPrompt = {
* Convert the user-entered key into base64.
*
* @param {string} keyString - The key to convert.
* @returns {string?} - The base64 representation, or undefined if the given
* @returns {?string} - The base64 representation, or undefined if the given
* key was not the correct format.
*/
_keyToBase64(keyString) {
......@@ -310,14 +327,17 @@ var OnionAuthPrompt = {
maxLogLevelPref: "browser.onionAuthPrompt.loglevel",
});
const { TorStrings } = ChromeUtils.importESModule(
"resource://gre/modules/TorStrings.sys.mjs"
);
this.TorStrings = TorStrings;
ChromeUtils.defineESModuleGetters(this._lazy, {
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
CommonUtils: "resource://services-common/utils.sys.mjs",
});
// Allow synchornous access to the localized strings. Used only for the
// button actions, which is currently a hard requirement for
// PopupNotifications.show. Hopefully, PopupNotifications will accept fluent
// ids in their place, or get replaced with something else that does.
ChromeUtils.defineLazyGetter(this._lazy, "SyncL10n", () => {
return new Localization(["toolkit/global/tor-browser.ftl"], true);
});
this._keyInput = document.getElementById("tor-clientauth-notification-key");
this._persistCheckbox = document.getElementById(
......@@ -328,19 +348,6 @@ var OnionAuthPrompt = {
"tor-clientauth-notification-desc"
);
// Set "Learn More" label and href.
const learnMoreElem = document.getElementById(
"tor-clientauth-notification-learnmore"
);
learnMoreElem.setAttribute(
"value",
this.TorStrings.onionServices.learnMore
);
this._keyInput.setAttribute(
"placeholder",
this.TorStrings.onionServices.authPrompt.keyPlaceholder
);
this._keyInput.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
......@@ -349,7 +356,7 @@ var OnionAuthPrompt = {
});
this._keyInput.addEventListener("input", () => {
// Remove the warning.
this._showWarning(undefined);
this._showWarning(null);
});
// Force back focus on click: tor-browser#41856
......
/* Copyright (c) 2020, The Tor Project, Inc. */
@namespace html url("http://www.w3.org/1999/xhtml");
html|*#tor-clientauth-notification-onionname {
#tor-clientauth-notification-desc {
font-weight: bold;
}
html|*#tor-clientauth-notification-key {
#tor-clientauth-notification-key {
box-sizing: border-box;
width: 100%;
margin-top: 15px;
......@@ -17,12 +15,12 @@ html|*#tor-clientauth-notification-key {
* browser/components/newtab/css/activity-stream-mac.css (linux and windows
* use the same rules).
*/
html|*#tor-clientauth-notification-key.invalid {
#tor-clientauth-notification-key.invalid {
border: 1px solid #D70022;
box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3);
}
html|*#tor-clientauth-warning {
#tor-clientauth-warning {
display: inline-block;
animation: fade-up-tt 450ms;
background: #D70022;
......@@ -35,11 +33,11 @@ html|*#tor-clientauth-warning {
z-index: 1;
}
html|*#tor-clientauth-warning[hidden] {
#tor-clientauth-warning[hidden] {
display: none;
}
html|*#tor-clientauth-warning::before {
#tor-clientauth-warning::before {
background: #D70022;
bottom: -8px;
content: '.';
......
......@@ -3,23 +3,10 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
TorStrings: "resource://gre/modules/TorStrings.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});
var gOnionServicesSavedKeysDialog = {
selector: {
dialog: "#onionservices-savedkeys-dialog",
intro: "#onionservices-savedkeys-intro",
tree: "#onionservices-savedkeys-tree",
onionSiteCol: "#onionservices-savedkeys-siteCol",
onionKeyCol: "#onionservices-savedkeys-keyCol",
errorIcon: "#onionservices-savedkeys-errorIcon",
errorMessage: "#onionservices-savedkeys-errorMessage",
removeButton: "#onionservices-savedkeys-remove",
removeAllButton: "#onionservices-savedkeys-removeall",
},
_tree: undefined,
_busyCount: 0,
get _isBusy() {
......@@ -27,8 +14,8 @@ var gOnionServicesSavedKeysDialog = {
return this._busyCount > 0;
},
// Public functions (called from outside this file).
async deleteSelectedKeys() {
async _deleteSelectedKeys() {
this._showError(null);
this._withBusy(async () => {
const indexesToDelete = [];
const count = this._tree.view.selection.getRangeCount();
......@@ -42,8 +29,6 @@ var gOnionServicesSavedKeysDialog = {
}
if (indexesToDelete.length) {
const controllerFailureMsg =
TorStrings.onionServices.authPreferences.failedToRemoveKey;
const provider = await TorProviderBuilder.build();
try {
// Remove in reverse index order to avoid issues caused by index
......@@ -53,28 +38,23 @@ var gOnionServicesSavedKeysDialog = {
}
} catch (e) {
console.error("Removing a saved key failed", e);
if (e.torMessage) {
this._showError(e.torMessage);
} else {
this._showError(controllerFailureMsg);
}
this._showError(
"onion-site-saved-keys-dialog-remove-keys-error-message"
);
}
}
});
},
async deleteAllKeys() {
async _deleteAllKeys() {
this._tree.view.selection.selectAll();
await this.deleteSelectedKeys();
await this._deleteSelectedKeys();
},
updateButtonsState() {
_updateButtonsState() {
const haveSelection = this._tree.view.selection.getRangeCount() > 0;
const dialog = document.querySelector(this.selector.dialog);
const removeSelectedBtn = dialog.querySelector(this.selector.removeButton);
removeSelectedBtn.disabled = this._isBusy || !haveSelection;
const removeAllBtn = dialog.querySelector(this.selector.removeAllButton);
removeAllBtn.disabled = this._isBusy || this.rowCount === 0;
this._removeButton.disabled = this._isBusy || !haveSelection;
this._removeAllButton.disabled = this._isBusy || this.rowCount === 0;
},
// Private functions.
......@@ -82,38 +62,40 @@ var gOnionServicesSavedKeysDialog = {
document.mozSubdialogReady = this._init();
},
async _init() {
_init() {
this._populateXUL();
window.addEventListener("keypress", this._onWindowKeyPress.bind(this));
this._loadSavedKeys();
},
_populateXUL() {
const dialog = document.querySelector(this.selector.dialog);
const authPrefStrings = TorStrings.onionServices.authPreferences;
dialog.setAttribute("title", authPrefStrings.dialogTitle);
let elem = dialog.querySelector(this.selector.intro);
elem.textContent = authPrefStrings.dialogIntro;
elem = dialog.querySelector(this.selector.onionSiteCol);
elem.setAttribute("label", authPrefStrings.onionSite);
elem = dialog.querySelector(this.selector.onionKeyCol);
elem.setAttribute("label", authPrefStrings.onionKey);
elem = dialog.querySelector(this.selector.removeButton);
elem.setAttribute("label", authPrefStrings.remove);
elem = dialog.querySelector(this.selector.removeAllButton);
elem.setAttribute("label", authPrefStrings.removeAll);
this._errorMessageContainer = document.getElementById(
"onionservices-savedkeys-errorContainer"
);
this._errorMessageEl = document.getElementById(
"onionservices-savedkeys-errorMessage"
);
this._removeButton = document.getElementById(
"onionservices-savedkeys-remove"
);
this._removeButton.addEventListener("click", () => {
this._deleteSelectedKeys();
});
this._removeAllButton = document.getElementById(
"onionservices-savedkeys-removeall"
);
this._removeButton.addEventListener("click", () => {
this._deleteAllKeys();
});
this._tree = dialog.querySelector(this.selector.tree);
this._tree = document.getElementById("onionservices-savedkeys-tree");
this._tree.addEventListener("select", () => {
this._updateButtonsState();
});
},
async _loadSavedKeys() {
const controllerFailureMsg =
TorStrings.onionServices.authPreferences.failedToGetKeys;
this._showError(null);
this._withBusy(async () => {
try {
this._tree.view = this;
......@@ -139,11 +121,10 @@ var gOnionServicesSavedKeysDialog = {
// Render the tree content.
this._tree.rowCountChanged(0, this.rowCount);
} catch (e) {
if (e.torMessage) {
this._showError(e.torMessage);
} else {
this._showError(controllerFailureMsg);
}
console.error("Failed to load keys", e);
this._showError(
"onion-site-saved-keys-dialog-fetch-keys-error-message"
);
}
});
},
......@@ -160,14 +141,14 @@ var gOnionServicesSavedKeysDialog = {
async _withBusy(func) {
this._busyCount++;
if (this._busyCount === 1) {
this.updateButtonsState();
this._updateButtonsState();
}
try {
await func();
} finally {
this._busyCount--;
if (this._busyCount === 0) {
this.updateButtonsState();
this._updateButtonsState();
}
}
},
......@@ -179,16 +160,25 @@ var gOnionServicesSavedKeysDialog = {
if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
window.close();
} else if (event.keyCode === KeyEvent.DOM_VK_DELETE) {
this.deleteSelectedKeys();
this._deleteSelectedKeys();
}
},
_showError(aMessage) {
document
.getElementById("onionservices-savedkeys-errorContainer")
.classList.toggle("show-error", !!aMessage);
const errorDesc = document.querySelector(this.selector.errorMessage);
errorDesc.textContent = aMessage ? aMessage : "";
/**
* Show an error, or clear it.
*
* @param {?string} messageId - The l10n ID of the message to show, or null to
* clear it.
*/
_showError(messageId) {
this._errorMessageContainer.classList.toggle("show-error", !!messageId);
if (messageId) {
document.l10n.setAttributes(this._errorMessageEl, messageId);
} else {
// Clean up.
this._errorMessageEl.removeAttribute("data-l10n-id");
this._errorMessageEl.textContent = "";
}
},
// XUL tree widget view implementation.
......
......@@ -9,29 +9,37 @@
id="onionservices-savedkeys-dialog"
windowtype="OnionServices:SavedKeys"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
data-l10n-id="onion-site-saved-keys-dialog-title"
>
<linkset>
<html:link rel="localization" href="toolkit/global/tor-browser.ftl" />
</linkset>
<script src="chrome://browser/content/onionservices/savedKeysDialog.js" />
<vbox id="onionservices-savedkeys" class="contentPane" flex="1">
<label
id="onionservices-savedkeys-intro"
control="onionservices-savedkeys-tree"
data-l10n-id="onion-site-saved-keys-dialog-intro"
/>
<separator class="thin" />
<tree
id="onionservices-savedkeys-tree"
flex="1"
hidecolumnpicker="true"
onselect="gOnionServicesSavedKeysDialog.updateButtonsState();"
>
<tree id="onionservices-savedkeys-tree" flex="1" hidecolumnpicker="true">
<treecols>
<treecol
id="onionservices-savedkeys-siteCol"
flex="1"
persist="width"
data-l10n-id="onion-site-saved-keys-dialog-table-header-site"
/>
<splitter class="tree-splitter" />
<treecol id="onionservices-savedkeys-keyCol" flex="1" persist="width" />
<treecol
id="onionservices-savedkeys-keyCol"
flex="1"
persist="width"
data-l10n-id="onion-site-saved-keys-dialog-table-header-key"
/>
</treecols>
<treechildren />
</tree>
......@@ -41,15 +49,15 @@
</hbox>
<separator class="thin" />
<hbox id="onionservices-savedkeys-buttons">
<button
<html:button
id="onionservices-savedkeys-remove"
disabled="true"
oncommand="gOnionServicesSavedKeysDialog.deleteSelectedKeys();"
/>
<button
data-l10n-id="onion-site-saved-keys-dialog-remove-button"
></html:button>
<html:button
id="onionservices-savedkeys-removeall"
oncommand="gOnionServicesSavedKeysDialog.deleteAllKeys();"
/>
data-l10n-id="onion-site-saved-keys-dialog-remove-all-button"
></html:button>
</hbox>
</vbox>
</window>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment