
ma1 pushed to branch tor-browser-128.11.0esr-14.5-1 at The Tor Project / Applications / Tor Browser Commits: 62b83143 by Henry Wilkes at 2025-05-27T10:28:07+01:00 fixup! BB 40925: Implemented the Security Level component TB 43783: Prompt user for a restart if their security level preferences are not aligned at startup or mid-session. Also handle failures to apply NoScript settings. - - - - - 02472976 by Henry Wilkes at 2025-05-27T10:28:08+01:00 fixup! BB 40069: Add helpers for message passing with extensions TB 43783: Allow the browser to wait for the NoScript settings to be applied. - - - - - 84244779 by Henry Wilkes at 2025-05-27T10:28:09+01:00 fixup! Base Browser strings TB 43783: Add security level prompt strings. - - - - - 7a8bfa8e by Henry Wilkes at 2025-05-27T10:28:10+01:00 fixup! TB 40026 [android]: Implement Security Level settings on Android. TB 43783: Expose SecurityLevelPrefs.setSecurityLevelAndRestart to android integration. - - - - - 8d29eede by Henry Wilkes at 2025-05-28T15:35:57+01:00 fixup! Base Browser strings TB 43782: Add strings for new security level UX flow. - - - - - 298a0418 by Henry Wilkes at 2025-05-28T15:35:58+01:00 fixup! BB 40925: Implemented the Security Level component TB 43782: Update security level UI for new UX flow. In addition, we drop the distinction between the security levels in the UI when the user has a custom security level. I.e. we always show shield as unfilled but with a yellow dot in the toolbar, and we just call it "Custom" rather than "Standard Custom", etc. - - - - - 19 changed files: - browser/components/BrowserGlue.sys.mjs - + browser/components/securitylevel/SecurityLevelUIUtils.sys.mjs - browser/components/securitylevel/content/securityLevel.js - browser/components/securitylevel/content/securityLevelButton.css - + browser/components/securitylevel/content/securityLevelDialog.js - + browser/components/securitylevel/content/securityLevelDialog.xhtml - browser/components/securitylevel/content/securityLevelPanel.css - browser/components/securitylevel/content/securityLevelPanel.inc.xhtml - browser/components/securitylevel/content/securityLevelPreferences.css - browser/components/securitylevel/content/securityLevelPreferences.inc.xhtml - browser/components/securitylevel/jar.mn - browser/components/securitylevel/moz.build - + browser/modules/SecurityLevelRestartNotification.sys.mjs - browser/modules/moz.build - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorAndroidIntegration.java - toolkit/components/extensions/ExtensionParent.sys.mjs - toolkit/components/securitylevel/SecurityLevel.sys.mjs - toolkit/locales/en-US/toolkit/global/base-browser.ftl - toolkit/modules/TorAndroidIntegration.sys.mjs Changes: ===================================== browser/components/BrowserGlue.sys.mjs ===================================== @@ -82,6 +82,8 @@ ChromeUtils.defineESModuleGetters(lazy, { ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SecurityLevelRestartNotification: + "resource:///modules/SecurityLevelRestartNotification.sys.mjs", SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs", SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", ShellService: "resource:///modules/ShellService.sys.mjs", @@ -2030,6 +2032,8 @@ BrowserGlue.prototype = { lazy.DragDropFilter.init(); + lazy.SecurityLevelRestartNotification.ready(); + lazy.TorProviderBuilder.firstWindowLoaded(); lazy.TorSettingsNotification.ready(); ===================================== browser/components/securitylevel/SecurityLevelUIUtils.sys.mjs ===================================== @@ -0,0 +1,73 @@ +/** + * Common methods for the desktop security level components. + */ +export const SecurityLevelUIUtils = { + /** + * Create an element that gives a description of the security level. To be + * used in the settings. + * + * @param {string} level - The security level to describe. + * @param {Document} doc - The document where the element will be inserted. + * + * @returns {Element} - The newly created element. + */ + createDescriptionElement(level, doc) { + const el = doc.createElement("div"); + el.classList.add("security-level-description"); + + let l10nIdSummary; + let bullets; + switch (level) { + case "standard": + l10nIdSummary = "security-level-summary-standard"; + break; + case "safer": + l10nIdSummary = "security-level-summary-safer"; + bullets = [ + "security-level-preferences-bullet-https-only-javascript", + "security-level-preferences-bullet-limit-font-and-symbols", + "security-level-preferences-bullet-limit-media", + ]; + break; + case "safest": + l10nIdSummary = "security-level-summary-safest"; + bullets = [ + "security-level-preferences-bullet-disabled-javascript", + "security-level-preferences-bullet-limit-font-and-symbols-and-images", + "security-level-preferences-bullet-limit-media", + ]; + break; + case "custom": + l10nIdSummary = "security-level-summary-custom"; + break; + default: + throw Error(`Unhandled level: ${level}`); + } + + const summaryEl = doc.createElement("div"); + summaryEl.classList.add("security-level-summary"); + doc.l10n.setAttributes(summaryEl, l10nIdSummary); + + el.append(summaryEl); + + if (!bullets) { + return el; + } + + const listEl = doc.createElement("ul"); + listEl.classList.add("security-level-description-extra"); + // Add a mozilla styling class as well: + listEl.classList.add("privacy-extra-information"); + for (const l10nId of bullets) { + const bulletEl = doc.createElement("li"); + bulletEl.classList.add("security-level-description-bullet"); + + doc.l10n.setAttributes(bulletEl, l10nId); + + listEl.append(bulletEl); + } + + el.append(listEl); + return el; + }, +}; ===================================== browser/components/securitylevel/content/securityLevel.js ===================================== @@ -1,9 +1,10 @@ "use strict"; -/* global AppConstants, Services, openPreferences, XPCOMUtils */ +/* global AppConstants, Services, openPreferences, XPCOMUtils, gSubDialog */ ChromeUtils.defineESModuleGetters(this, { SecurityLevelPrefs: "resource://gre/modules/SecurityLevel.sys.mjs", + SecurityLevelUIUtils: "resource:///modules/SecurityLevelUIUtils.sys.mjs", }); /* @@ -35,12 +36,8 @@ var SecurityLevelButton = { _anchorButton: null, _configUIFromPrefs() { - const level = SecurityLevelPrefs.securityLevel; - if (!level) { - return; - } - const custom = SecurityLevelPrefs.securityCustom; - this._button.setAttribute("level", custom ? `${level}_custom` : level); + const level = SecurityLevelPrefs.securityLevelSummary; + this._button.setAttribute("level", level); let l10nIdLevel; switch (level) { @@ -53,15 +50,12 @@ var SecurityLevelButton = { case "safest": l10nIdLevel = "security-level-toolbar-button-safest"; break; + case "custom": + l10nIdLevel = "security-level-toolbar-button-custom"; + break; default: throw Error(`Unhandled level: ${level}`); } - if (custom) { - // Don't distinguish between the different levels when in the custom - // state. We just want to emphasise that it is custom rather than any - // specific level. - l10nIdLevel = "security-level-toolbar-button-custom"; - } document.l10n.setAttributes(this._button, l10nIdLevel); }, @@ -164,12 +158,7 @@ var SecurityLevelPanel = { panel: document.getElementById("securityLevel-panel"), background: document.getElementById("securityLevel-background"), levelName: document.getElementById("securityLevel-level"), - customName: document.getElementById("securityLevel-custom"), summary: document.getElementById("securityLevel-summary"), - restoreDefaultsButton: document.getElementById( - "securityLevel-restoreDefaults" - ), - settingsButton: document.getElementById("securityLevel-settings"), }; const learnMoreEl = document.getElementById("securityLevel-learnMore"); @@ -177,12 +166,11 @@ var SecurityLevelPanel = { this.hide(); }); - this._elements.restoreDefaultsButton.addEventListener("command", () => { - this.restoreDefaults(); - }); - this._elements.settingsButton.addEventListener("command", () => { - this.openSecuritySettings(); - }); + document + .getElementById("securityLevel-settings") + .addEventListener("command", () => { + this.openSecuritySettings(); + }); this._elements.panel.addEventListener("popupshown", () => { // Bring focus into the panel by focusing the default button. @@ -199,19 +187,7 @@ var SecurityLevelPanel = { } // get security prefs - const level = SecurityLevelPrefs.securityLevel; - const custom = SecurityLevelPrefs.securityCustom; - - // only visible when user is using custom settings - this._elements.customName.hidden = !custom; - this._elements.restoreDefaultsButton.hidden = !custom; - if (custom) { - this._elements.settingsButton.removeAttribute("default"); - this._elements.restoreDefaultsButton.setAttribute("default", "true"); - } else { - this._elements.settingsButton.setAttribute("default", "true"); - this._elements.restoreDefaultsButton.removeAttribute("default"); - } + const level = SecurityLevelPrefs.securityLevelSummary; // Descriptions change based on security level this._elements.background.setAttribute("level", level); @@ -230,12 +206,13 @@ var SecurityLevelPanel = { l10nIdLevel = "security-level-panel-level-safest"; l10nIdSummary = "security-level-summary-safest"; break; + case "custom": + l10nIdLevel = "security-level-panel-level-custom"; + l10nIdSummary = "security-level-summary-custom"; + break; default: throw Error(`Unhandled level: ${level}`); } - if (custom) { - l10nIdSummary = "security-level-summary-custom"; - } document.l10n.setAttributes(this._elements.levelName, l10nIdLevel); document.l10n.setAttributes(this._elements.summary, l10nIdSummary); @@ -269,13 +246,6 @@ var SecurityLevelPanel = { this._elements.panel.hidePopup(); }, - restoreDefaults() { - SecurityLevelPrefs.securityCustom = false; - // Move focus to the settings button since restore defaults button will - // become hidden. - this._elements.settingsButton.focus(); - }, - openSecuritySettings() { openPreferences("privacy-securitylevel"); this.hide(); @@ -301,59 +271,64 @@ var SecurityLevelPanel = { var SecurityLevelPreferences = { _securityPrefsBranch: null, + /** - * The notification box shown when the user has a custom security setting. - * - * @type {Element} - */ - _customNotification: null, - /** - * The radiogroup for this preference. - * - * @type {Element} - */ - _radiogroup: null, - /** - * A list of radio options and their containers. + * The element that shows the current security level. * - * @type {Array<object>} + * @type {?Element} */ - _radioOptions: null, + _currentEl: null, _populateXUL() { - this._customNotification = document.getElementById( - "securityLevel-customNotification" + this._currentEl = document.getElementById("security-level-current"); + const changeButton = document.getElementById("security-level-change"); + const badgeEl = this._currentEl.querySelector( + ".security-level-current-badge" ); - document - .getElementById("securityLevel-restoreDefaults") - .addEventListener("command", () => { - SecurityLevelPrefs.securityCustom = false; - }); - - this._radiogroup = document.getElementById("securityLevel-radiogroup"); - this._radioOptions = Array.from( - this._radiogroup.querySelectorAll(".securityLevel-radio-option"), - container => { - return { container, radio: container.querySelector("radio") }; - } - ); + for (const { level, nameId } of [ + { level: "standard", nameId: "security-level-panel-level-standard" }, + { level: "safer", nameId: "security-level-panel-level-safer" }, + { level: "safest", nameId: "security-level-panel-level-safest" }, + { level: "custom", nameId: "security-level-panel-level-custom" }, + ]) { + // Classes that control visibility: + // security-level-current-standard + // security-level-current-safer + // security-level-current-safest + // security-level-current-custom + const visibilityClass = `security-level-current-${level}`; + const nameEl = document.createElement("div"); + nameEl.classList.add("security-level-name", visibilityClass); + document.l10n.setAttributes(nameEl, nameId); + + const descriptionEl = SecurityLevelUIUtils.createDescriptionElement( + level, + document + ); + descriptionEl.classList.add(visibilityClass); + + this._currentEl.insertBefore(nameEl, badgeEl); + this._currentEl.insertBefore(descriptionEl, changeButton); + } - this._radiogroup.addEventListener("select", () => { - SecurityLevelPrefs.securityLevel = this._radiogroup.value; + changeButton.addEventListener("click", () => { + this._openDialog(); }); }, + _openDialog() { + gSubDialog.open( + "chrome://browser/content/securitylevel/securityLevelDialog.xhtml", + { features: "resizable=yes" } + ); + }, + _configUIFromPrefs() { - this._radiogroup.value = SecurityLevelPrefs.securityLevel; - const isCustom = SecurityLevelPrefs.securityCustom; - this._radiogroup.disabled = isCustom; - this._customNotification.hidden = !isCustom; - // Have the container's selection CSS class match the selection state of the - // radio elements. - for (const { container, radio } of this._radioOptions) { - container.classList.toggle("selected", radio.selected); - } + // Set a data-current-level attribute for showing the current security + // level, and hiding the rest. + this._currentEl.dataset.currentLevel = + SecurityLevelPrefs.securityLevelSummary; }, init() { ===================================== browser/components/securitylevel/content/securityLevelButton.css ===================================== @@ -7,12 +7,6 @@ toolbarbutton#security-level-button[level="safer"] { toolbarbutton#security-level-button[level="safest"] { list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest"); } -toolbarbutton#security-level-button[level="standard_custom"] { +toolbarbutton#security-level-button[level="custom"] { list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard_custom"); } -toolbarbutton#security-level-button[level="safer_custom"] { - list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safer_custom"); -} -toolbarbutton#security-level-button[level="safest_custom"] { - list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest_custom"); -} ===================================== browser/components/securitylevel/content/securityLevelDialog.js ===================================== @@ -0,0 +1,188 @@ +"use strict"; + +const { SecurityLevelPrefs } = ChromeUtils.importESModule( + "resource://gre/modules/SecurityLevel.sys.mjs" +); +const { SecurityLevelUIUtils } = ChromeUtils.importESModule( + "resource:///modules/SecurityLevelUIUtils.sys.mjs" +); + +const gSecurityLevelDialog = { + /** + * The security level when this dialog was opened. + * + * @type {string} + */ + _prevLevel: SecurityLevelPrefs.securityLevelSummary, + /** + * The security level currently selected. + * + * @type {string} + */ + _selectedLevel: "", + /** + * The radiogroup for this preference. + * + * @type {?Element} + */ + _radiogroup: null, + /** + * A list of radio options and their containers. + * + * @type {?Array<{ container: Element, radio: Element }>} + */ + _radioOptions: null, + + /** + * Initialise the dialog. + */ + async init() { + const dialog = document.getElementById("security-level-dialog"); + dialog.addEventListener("dialogaccept", event => { + if (this._acceptButton.disabled) { + event.preventDefault(); + return; + } + this._commitChange(); + }); + + this._acceptButton = dialog.getButton("accept"); + + document.l10n.setAttributes( + this._acceptButton, + "security-level-dialog-save-restart" + ); + + this._radiogroup = document.getElementById("security-level-radiogroup"); + + this._radioOptions = Array.from( + this._radiogroup.querySelectorAll(".security-level-radio-container"), + container => { + return { + container, + radio: container.querySelector(".security-level-radio"), + }; + } + ); + + for (const { container, radio } of this._radioOptions) { + const level = radio.value; + radio.id = `security-level-radio-${level}`; + const currentEl = container.querySelector( + ".security-level-current-badge" + ); + currentEl.id = `security-level-current-badge-${level}`; + const descriptionEl = SecurityLevelUIUtils.createDescriptionElement( + level, + document + ); + descriptionEl.classList.add("indent"); + descriptionEl.id = `security-level-description-${level}`; + + // Wait for the full translation of the element before adding it to the + // DOM. In particular, we want to make sure the elements have text before + // we measure the maxHeight below. + await document.l10n.translateFragment(descriptionEl); + document.l10n.pauseObserving(); + container.append(descriptionEl); + document.l10n.resumeObserving(); + + if (level === this._prevLevel) { + currentEl.hidden = false; + // When the currentEl is visible, include it in the accessible name for + // the radio option. + // NOTE: The currentEl has an accessible name which includes punctuation + // to help separate it's content from the security level name. + // E.g. "Standard (Current level)". + radio.setAttribute("aria-labelledby", `${radio.id} ${currentEl.id}`); + } else { + currentEl.hidden = true; + } + // We point the accessible description to the wrapping + // .security-level-description element, rather than its children + // that define the actual text content. This means that when the + // privacy-extra-information is shown or hidden, its text content is + // included or excluded from the accessible description, respectively. + radio.setAttribute("aria-describedby", descriptionEl.id); + } + + // We want to reserve the maximum height of the radiogroup so that the + // dialog has enough height when the user switches options. So we cycle + // through the options and measure the height when they are selected to set + // a minimum height that fits all of them. + // NOTE: At the time of implementation, at this point the dialog may not + // yet have the "subdialog" attribute, which means it is missing the + // common.css stylesheet from its shadow root, which effects the size of the + // .radio-check element and the font. Therefore, we have duplicated the + // import of common.css in SecurityLevelDialog.xhtml to ensure it is applied + // at this earlier stage. + let maxHeight = 0; + for (const { container } of this._radioOptions) { + container.classList.add("selected"); + maxHeight = Math.max( + maxHeight, + this._radiogroup.getBoundingClientRect().height + ); + container.classList.remove("selected"); + } + this._radiogroup.style.minHeight = `${maxHeight}px`; + + if (this._prevLevel !== "custom") { + this._selectedLevel = this._prevLevel; + this._radiogroup.value = this._prevLevel; + } else { + this._radiogroup.selectedItem = null; + } + + this._radiogroup.addEventListener("select", () => { + this._selectedLevel = this._radiogroup.value; + this._updateSelected(); + }); + + this._updateSelected(); + }, + + /** + * Update the UI in response to a change in selection. + */ + _updateSelected() { + this._acceptButton.disabled = + !this._selectedLevel || this._selectedLevel === this._prevLevel; + // Have the container's `selected` CSS class match the selection state of + // the radio elements. + for (const { container, radio } of this._radioOptions) { + container.classList.toggle("selected", radio.selected); + } + }, + + /** + * Commit the change in security level and restart the browser. + */ + _commitChange() { + SecurityLevelPrefs.setSecurityLevelBeforeRestart(this._selectedLevel); + Services.startup.quit( + Services.startup.eAttemptQuit | Services.startup.eRestart + ); + }, +}; + +// Initial focus is not visible, even if opened with a keyboard. We avoid the +// default handler and manage the focus ourselves, which will paint the focus +// ring by default. +// NOTE: A side effect is that the focus ring will show even if the user opened +// with a mouse event. +// TODO: Remove this once bugzilla bug 1708261 is resolved. +document.subDialogSetDefaultFocus = () => { + document.getElementById("security-level-radiogroup").focus(); +}; + +// Delay showing and sizing the subdialog until it is fully initialised. +document.mozSubdialogReady = new Promise(resolve => { + window.addEventListener( + "DOMContentLoaded", + () => { + gSecurityLevelDialog.init().finally(resolve); + }, + { once: true } + ); +}); ===================================== browser/components/securitylevel/content/securityLevelDialog.xhtml ===================================== @@ -0,0 +1,86 @@ +<?xml version="1.0"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="security-level-dialog-window" +> + <dialog id="security-level-dialog" buttons="accept,cancel"> + <linkset> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <!-- NOTE: We include common.css explicitly, rather than relying on + - the dialog's shadowroot importing it, which is late loaded in + - response to the dialog's "subdialog" attribute, which is set + - in response to DOMFrameContentLoaded. + - In particular, we need the .radio-check rule and font rules from + - common-shared.css to be in place when gSecurityLevelDialog.init is + - called, which will help ensure that the radio element has the correct + - size when we measure its bounding box. --> + <html:link + rel="stylesheet" + href="chrome://global/skin/in-content/common.css" + /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/preferences/preferences.css" + /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/preferences/privacy.css" + /> + <html:link + rel="stylesheet" + href="chrome://browser/content/securitylevel/securityLevelPreferences.css" + /> + + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link rel="localization" href="toolkit/global/base-browser.ftl" /> + </linkset> + + <script src="chrome://browser/content/securitylevel/securityLevelDialog.js" /> + + <description data-l10n-id="security-level-dialog-restart-description" /> + + <radiogroup id="security-level-radiogroup" class="highlighting-group"> + <html:div + class="security-level-radio-container security-level-grid privacy-detailedoption info-box-container" + > + <radio + class="security-level-radio security-level-name" + value="standard" + data-l10n-id="security-level-preferences-level-standard" + /> + <html:div + class="security-level-current-badge" + data-l10n-id="security-level-preferences-current-badge" + ></html:div> + </html:div> + <html:div + class="security-level-radio-container security-level-grid privacy-detailedoption info-box-container" + > + <radio + class="security-level-radio security-level-name" + value="safer" + data-l10n-id="security-level-preferences-level-safer" + /> + <html:div + class="security-level-current-badge" + data-l10n-id="security-level-preferences-current-badge" + ></html:div> + </html:div> + <html:div + class="security-level-radio-container security-level-grid privacy-detailedoption info-box-container" + > + <radio + class="security-level-radio security-level-name" + value="safest" + data-l10n-id="security-level-preferences-level-safest" + /> + <html:div + class="security-level-current-badge" + data-l10n-id="security-level-preferences-current-badge" + ></html:div> + </html:div> + </radiogroup> + </dialog> +</window> ===================================== browser/components/securitylevel/content/securityLevelPanel.css ===================================== @@ -23,7 +23,7 @@ background-position-x: right var(--background-inline-offset); } -#securityLevel-background[level="standard"] { +#securityLevel-background:is([level="standard"], [level="custom"]) { background-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard"); } @@ -49,14 +49,6 @@ font-weight: 600; } -#securityLevel-custom { - border-radius: 4px; - background-color: var(--warning-icon-bgcolor); - color: black; - padding: 0.4em 0.5em; - margin-inline-start: 1em; -} - #securityLevel-summary { padding-inline-end: 5em; max-width: 20em; ===================================== browser/components/securitylevel/content/securityLevelPanel.inc.xhtml ===================================== @@ -15,10 +15,6 @@ <vbox id="securityLevel-background" class="panel-subview-body"> <html:p id="securityLevel-subheading"> <html:span id="securityLevel-level"></html:span> - <html:span - id="securityLevel-custom" - data-l10n-id="security-level-panel-custom-badge" - ></html:span> </html:p> <html:p id="securityLevel-summary"></html:p> <html:a @@ -32,12 +28,8 @@ <button id="securityLevel-settings" class="footer-button" + default="true" data-l10n-id="security-level-panel-open-settings-button" /> - <button - id="securityLevel-restoreDefaults" - class="footer-button" - data-l10n-id="security-level-restore-defaults-button" - /> </html:moz-button-group> </panel> ===================================== browser/components/securitylevel/content/securityLevelPreferences.css ===================================== @@ -1,26 +1,166 @@ -#securityLevel-groupbox { - --section-highlight-background-color: color-mix(in srgb, var(--in-content-accent-color) 20%, transparent); +.security-level-grid { + display: grid; + grid-template: + "icon name badge button" min-content + "icon summary summary button" auto + "icon extra extra ." auto + / max-content max-content 1fr max-content; } -#securityLevel-customNotification { - /* Spacing similar to #fpiIncompatibilityWarning. */ - margin-block: 16px; +.security-level-icon { + grid-area: icon; + align-self: start; + width: 24px; + height: 24px; + -moz-context-properties: fill; + fill: var(--in-content-icon-color); + margin-block-start: var(--space-xsmall); + margin-inline-end: var(--space-large); } -.info-icon.securityLevel-custom-warning-icon { - list-style-image: url("chrome://global/skin/icons/warning.svg"); +.security-level-current-badge { + grid-area: badge; + align-self: center; + justify-self: start; + white-space: nowrap; + background: var(--background-color-information); + color: inherit; + font-size: var(--font-size-small); + border-radius: var(--border-radius-circle); + margin-inline-start: var(--space-small); + padding-block: var(--space-xsmall); + padding-inline: var(--space-small); } -#securityLevel-customHeading { +.security-level-current-badge span { + /* Still accessible to screen reader, but not visual. + * Keep inline, but with no layout width. */ + display: inline-block; + width: 1px; + margin-inline-end: -1px; + clip-path: inset(50%); +} + +@media (prefers-contrast) and (not (forced-colors)) { + .security-level-current-badge { + /* Match the checkbox/radio colors. */ + background: var(--color-accent-primary); + color: var(--button-text-color-primary); + } +} + +@media (forced-colors) { + .security-level-current-badge { + /* Match the checkbox/radio/selected colors. */ + background: SelectedItem; + color: SelectedItemText; + } +} + +.security-level-name { + grid-area: name; font-weight: bold; + align-self: center; + white-space: nowrap; +} + +.security-level-description { + display: grid; + grid-column: summary-start / extra-end; + grid-row: summary-start / extra-end; + grid-template-rows: subgrid; + grid-template-columns: subgrid; + margin-block-start: var(--space-small); +} + +.security-level-summary { + grid-area: summary; +} + +.security-level-description-extra { + grid-area: extra; + margin-block: var(--space-medium) 0; + margin-inline: var(--space-large) 0; + padding: 0; +} + +.security-level-description-bullet:not(:last-child) { + margin-block-end: var(--space-medium); +} + +/* Tweak current security level display. */ + +#security-level-current { + margin-block-start: var(--space-large); + background: var(--in-content-box-background); + border: 1px solid var(--in-content-box-border-color); + border-radius: var(--border-radius-small); + padding: var(--space-medium); +} + +#security-level-change { + grid-area: button; + align-self: center; + margin: 0; + margin-inline-start: var(--space-large); +} + +/* Adjust which content is visible depending on the current security level. */ + +#security-level-current:not([data-current-level="standard"]) .security-level-current-standard { + display: none; +} + +#security-level-current:not([data-current-level="safer"]) .security-level-current-safer { + display: none; +} + +#security-level-current:not([data-current-level="safest"]) .security-level-current-safest { + display: none; +} + +#security-level-current:not([data-current-level="custom"]) .security-level-current-custom { + display: none; +} + +#security-level-current[data-current-level="standard"] .security-level-icon { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard"); +} + +#security-level-current[data-current-level="safer"] .security-level-icon { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safer"); +} + +#security-level-current[data-current-level="safest"] .security-level-icon { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest"); +} + +#security-level-current[data-current-level="custom"] .security-level-icon { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard_custom"); +} + +/* Tweak security level dialog. */ + +#security-level-radiogroup { + margin-block: var(--space-large) var(--space-xlarge); +} + +.security-level-radio-container { + padding-block: var(--space-large); +} + +#security-level-radiogroup .security-level-radio { + margin: 0; } -#securityLevel-radiogroup[disabled] { - opacity: 0.5; +#security-level-radiogroup .radio-label-box { + /* .security-level-current-badge already has a margin. */ + margin: 0; } -/* Overwrite the rule in common-shared.css so we don't get 0.25 opacity overall - * on the radio text. */ -#securityLevel-radiogroup[disabled] radio[disabled] .radio-label-box { - opacity: 1.0; +#security-level-radiogroup .privacy-detailedoption.security-level-radio-container:not(.selected) .security-level-description-extra { + /* .privacy-detailedoption uses visibility: hidden, which does not work with + * our grid display (the margin is still reserved) so we use display: none + * instead. */ + display: none; } ===================================== browser/components/securitylevel/content/securityLevelPreferences.inc.xhtml ===================================== @@ -17,112 +17,19 @@ data-l10n-id="security-level-preferences-learn-more-link" ></html:a> </description> - <hbox - id="securityLevel-customNotification" - class="info-box-container" - flex="1" - > - <hbox class="info-icon-container"> - <image class="info-icon securityLevel-custom-warning-icon"/> - </hbox> - <vbox flex="1"> - <label - id="securityLevel-customHeading" - data-l10n-id="security-level-preferences-custom-heading" - /> - <description - id="securityLevel-customDescription" - data-l10n-id="security-level-summary-custom" - flex="1" - /> - </vbox> - <hbox align="center"> - <button - id="securityLevel-restoreDefaults" - data-l10n-id="security-level-restore-defaults-button" - /> - </hbox> - </hbox> - <radiogroup id="securityLevel-radiogroup"> - <vbox class="securityLevel-radio-option privacy-detailedoption info-box-container"> - <radio - value="standard" - data-l10n-id="security-level-preferences-level-standard" - aria-describedby="securityLevelSummary-standard" - /> - <vbox id="securityLevelSummary-standard" class="indent"> - <label data-l10n-id="security-level-summary-standard" /> - </vbox> - </vbox> - <vbox class="securityLevel-radio-option privacy-detailedoption info-box-container"> - <!-- NOTE: We point the accessible description to the wrapping vbox - - rather than its first description element. This means that when the - - privacy-extra-information is shown or hidden, its text content is - - included or excluded from the accessible description, respectively. - --> - <radio - value="safer" - data-l10n-id="security-level-preferences-level-safer" - aria-describedby="securityLevelSummary-safer" - /> - <vbox id="securityLevelSummary-safer" class="indent"> - <label data-l10n-id="security-level-summary-safer" /> - <vbox class="privacy-extra-information"> - <vbox class="indent"> - <hbox class="extra-information-label"> - <label - class="content-blocking-label" - data-l10n-id="security-level-preferences-bullet-https-only-javascript" - /> - </hbox> - <hbox class="extra-information-label"> - <label - class="content-blocking-label" - data-l10n-id="security-level-preferences-bullet-limit-font-and-symbols" - /> - </hbox> - <hbox class="extra-information-label"> - <label - class="content-blocking-label" - data-l10n-id="security-level-preferences-bullet-limit-media" - /> - </hbox> - </vbox> - </vbox> - </vbox> - </vbox> - <vbox class="securityLevel-radio-option privacy-detailedoption info-box-container"> - <radio - value="safest" - data-l10n-id="security-level-preferences-level-safest" - aria-describedby="securityLevelSummary-safest" - /> - <vbox id="securityLevelSummary-safest" class="indent"> - <label data-l10n-id="security-level-summary-safest" /> - <vbox class="privacy-extra-information"> - <vbox class="indent"> - <hbox class="extra-information-label"> - <label - class="content-blocking-label" - data-l10n-id="security-level-preferences-bullet-disabled-javascript" - /> - </hbox> - <hbox class="extra-information-label"> - <label - class="content-blocking-label" - data-l10n-id="security-level-preferences-bullet-limit-font-and-symbols-and-images" - /> - </hbox> - <hbox class="extra-information-label"> - <label - class="content-blocking-label" - data-l10n-id="security-level-preferences-bullet-limit-media" - /> - </hbox> - </vbox> - </vbox> - </vbox> - </vbox> - </radiogroup> + <html:div id="security-level-current" class="security-level-grid"> + <html:img + class="security-level-icon" + alt="" + /> + <html:div + class="security-level-current-badge" + data-l10n-id="security-level-preferences-current-badge" + ></html:div> + <html:button + id="security-level-change" + data-l10n-id="security-level-preferences-change-button" + ></html:button> + </html:div> </vbox> </groupbox> ===================================== browser/components/securitylevel/jar.mn ===================================== @@ -4,3 +4,5 @@ browser.jar: content/browser/securitylevel/securityLevelButton.css (content/securityLevelButton.css) content/browser/securitylevel/securityLevelPreferences.css (content/securityLevelPreferences.css) content/browser/securitylevel/securityLevelIcon.svg (content/securityLevelIcon.svg) + content/browser/securitylevel/securityLevelDialog.xhtml (content/securityLevelDialog.xhtml) + content/browser/securitylevel/securityLevelDialog.js (content/securityLevelDialog.js) ===================================== browser/components/securitylevel/moz.build ===================================== @@ -1 +1,5 @@ JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES += [ + "SecurityLevelUIUtils.sys.mjs", +] ===================================== browser/modules/SecurityLevelRestartNotification.sys.mjs ===================================== @@ -0,0 +1,72 @@ +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + SecurityLevelPrefs: "resource://gre/modules/SecurityLevel.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "NotificationStrings", function () { + return new Localization([ + "branding/brand.ftl", + "toolkit/global/base-browser.ftl", + ]); +}); + +/** + * Interface for showing the security level restart notification on desktop. + */ +export const SecurityLevelRestartNotification = { + /** + * Whether we have already been initialised. + * + * @type {boolean} + */ + _initialized: false, + + /** + * Called when the UI is ready to show a notification. + */ + ready() { + if (this._initialized) { + return; + } + this._initialized = true; + lazy.SecurityLevelPrefs.setRestartNotificationHandler(this); + }, + + /** + * Show the restart notification, and perform the restart if the user agrees. + */ + async tryRestartBrowser() { + const [titleText, bodyText, primaryButtonText, secondaryButtonText] = + await lazy.NotificationStrings.formatValues([ + { id: "security-level-restart-prompt-title" }, + { id: "security-level-restart-prompt-body" }, + { id: "security-level-restart-prompt-button-restart" }, + { id: "security-level-restart-prompt-button-ignore" }, + ]); + const buttonFlags = + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1; + + const propBag = await Services.prompt.asyncConfirmEx( + lazy.BrowserWindowTracker.getTopWindow()?.browsingContext ?? null, + Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, + titleText, + bodyText, + buttonFlags, + primaryButtonText, + secondaryButtonText, + null, + null, + null, + {} + ); + + if (propBag.get("buttonNumClicked") === 0) { + Services.startup.quit( + Services.startup.eAttemptQuit | Services.startup.eRestart + ); + } + }, +}; ===================================== browser/modules/moz.build ===================================== @@ -126,6 +126,7 @@ EXTRA_JS_MODULES += [ "PermissionUI.sys.mjs", "ProcessHangMonitor.sys.mjs", "Sanitizer.sys.mjs", + "SecurityLevelRestartNotification.sys.mjs", "SelectionChangedMenulist.sys.mjs", "SiteDataManager.sys.mjs", "SitePermissions.sys.mjs", ===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorAndroidIntegration.java ===================================== @@ -49,6 +49,9 @@ public class TorAndroidIntegration implements BundleEventListener { private static final String EVENT_SETTINGS_CHANGED = "GeckoView:Tor:SettingsChanged"; // Events we emit + // TODO: Integrate the security level API. tor-browser#43820 + private static final String EVENT_SECURITY_LEVEL_GET = "GeckoView:Tor:SecurityLevelGet"; + private static final String EVENT_SECURITY_LEVEL_SET_BEFORE_RESTART = "GeckoView:Tor:SecurityLevelSetBeforeRestart"; private static final String EVENT_SETTINGS_GET = "GeckoView:Tor:SettingsGet"; private static final String EVENT_SETTINGS_SET = "GeckoView:Tor:SettingsSet"; private static final String EVENT_BOOTSTRAP_BEGIN = "GeckoView:Tor:BootstrapBegin"; ===================================== toolkit/components/extensions/ExtensionParent.sys.mjs ===================================== @@ -2345,6 +2345,7 @@ async function torSendExtensionMessage(extensionId, message) { const result = await ProxyMessenger.conduit.castRuntimeMessage("messenger", { extensionId, holder: new StructuredCloneHolder("torSendExtensionMessage", null, message), + query: true, firstResponse: true, sender: { id: extensionId, ===================================== toolkit/components/securitylevel/SecurityLevel.sys.mjs ===================================== @@ -16,6 +16,7 @@ const BrowserTopics = Object.freeze({ // The Security Settings prefs in question. const kSliderPref = "browser.security_level.security_slider"; const kCustomPref = "browser.security_level.security_custom"; +const kNoScriptInitedPref = "browser.security_level.noscript_inited"; // __getPrefValue(prefName)__ // Returns the current value of a preference, regardless of its type. @@ -32,11 +33,11 @@ var getPrefValue = function (prefName) { } }; -// __bindPref(prefName, prefHandler, init)__ +// __bindPref(prefName, prefHandler)__ // Applies prefHandler whenever the value of the pref changes. // If init is true, applies prefHandler to the current value. -// Returns a zero-arg function that unbinds the pref. -var bindPref = function (prefName, prefHandler, init = false) { +// Returns the observer that was added. +var bindPref = function (prefName, prefHandler) { let update = () => { prefHandler(getPrefValue(prefName)); }, @@ -48,21 +49,9 @@ var bindPref = function (prefName, prefHandler, init = false) { }, }; Services.prefs.addObserver(prefName, observer); - if (init) { - update(); - } - return () => { - Services.prefs.removeObserver(prefName, observer); - }; + return observer; }; -// __bindPrefAndInit(prefName, prefHandler)__ -// Applies prefHandler to the current value of pref specified by prefName. -// Re-applies prefHandler whenever the value of the pref changes. -// Returns a zero-arg function that unbinds the pref. -var bindPrefAndInit = (prefName, prefHandler) => - bindPref(prefName, prefHandler, true); - async function waitForExtensionMessage(extensionId, checker = () => {}) { const { torWaitForExtensionMessage } = lazy.ExtensionParent; if (torWaitForExtensionMessage) { @@ -74,7 +63,7 @@ async function waitForExtensionMessage(extensionId, checker = () => {}) { async function sendExtensionMessage(extensionId, message) { const { torSendExtensionMessage } = lazy.ExtensionParent; if (torSendExtensionMessage) { - return torSendExtensionMessage(extensionId, message); + return await torSendExtensionMessage(extensionId, message); } return undefined; } @@ -187,14 +176,8 @@ var initializeNoScriptControl = () => { // `browser.runtime.onMessage.addListener(...)` in NoScript's bg/main.js. // TODO: Is there a better way? - let sendNoScriptSettings = settings => - sendExtensionMessage(noscriptID, settings); - - // __setNoScriptSafetyLevel(safetyLevel)__. - // Set NoScript settings according to a particular safety level - // (security slider level): 0 = Standard, 1 = Safer, 2 = Safest - let setNoScriptSafetyLevel = safetyLevel => - sendNoScriptSettings(noscriptSettings(safetyLevel)); + let sendNoScriptSettings = async settings => + await sendExtensionMessage(noscriptID, settings); // __securitySliderToSafetyLevel(sliderState)__. // Converts the "browser.security_level.security_slider" pref value @@ -204,36 +187,46 @@ var initializeNoScriptControl = () => { // Wait for the first message from NoScript to arrive, and then // bind the security_slider pref to the NoScript settings. - let messageListener = a => { + let messageListener = async a => { try { logger.debug("Message received from NoScript:", a); - let noscriptPersist = Services.prefs.getBoolPref( - "browser.security_level.noscript_persist", - false - ); + const persistPref = "browser.security_level.noscript_persist"; + let noscriptPersist = Services.prefs.getBoolPref(persistPref, false); let noscriptInited = Services.prefs.getBoolPref( - "browser.security_level.noscript_inited", + kNoScriptInitedPref, false ); - // Set the noscript safety level once if we have never run noscript - // before, or if we are not allowing noscript per-site settings to be - // persisted between browser sessions. Otherwise make sure that the - // security slider position, if changed, will rewrite the noscript - // settings. - bindPref( - kSliderPref, - sliderState => - setNoScriptSafetyLevel(securitySliderToSafetyLevel(sliderState)), - !noscriptPersist || !noscriptInited - ); - if (!noscriptInited) { - Services.prefs.setBoolPref( - "browser.security_level.noscript_inited", - true + // Set the noscript safety level once at startup. + // If a user has set noscriptPersist, then we only send this if the + // security level was changed in a previous session. + // NOTE: We do not re-send this when the security_slider preference + // changes mid-session because this should always require a restart. + if (noscriptPersist && noscriptInited) { + logger.warn( + `Not initialising NoScript since the user has set ${persistPref}` ); + return; } + // Read the security level, even if the user has the "custom" + // preference. + const securityIndex = Services.prefs.getIntPref(kSliderPref, 0); + const safetyLevel = securitySliderToSafetyLevel(securityIndex); + // May throw if NoScript fails to apply the settings: + const noscriptResult = await sendNoScriptSettings( + noscriptSettings(safetyLevel) + ); + // Mark the NoScript extension as initialised so we do not reset it + // at the next startup for noscript_persist users. + Services.prefs.setBoolPref(kNoScriptInitedPref, true); + logger.info("NoScript successfully initialised."); + // In the future NoScript may tell us more about how it applied our + // settings, e.g. if user is overriding per-site permissions. + // Up to NoScript 12.6 noscriptResult is undefined. + logger.debug("NoScript response:", noscriptResult); } catch (e) { - logger.exception(e); + logger.error("Could not apply NoScript settings", e); + // Treat as a custom security level for the rest of the session. + Services.prefs.setBoolPref(kCustomPref, true); } }; waitForExtensionMessage(noscriptID, a => a.__meta.name === "started").then( @@ -242,6 +235,8 @@ var initializeNoScriptControl = () => { logger.info("Listening for messages from NoScript."); } catch (e) { logger.exception(e); + // Treat as a custom security level for the rest of the session. + Services.prefs.setBoolPref(kCustomPref, true); } }; @@ -271,16 +266,60 @@ const kSecuritySettings = { // ### Prefs +/** + * Amend the security level index to a standard value. + * + * @param {integer} index - The input index value. + * @returns {integer} - A standard index value. + */ +function fixupIndex(index) { + if (!Number.isInteger(index) || index < 1 || index > 4) { + // Unexpected value out of range, go to the "safest" level as a fallback. + return 1; + } + if (index === 3) { + // Migrate from old medium-low (3) to new medium (2). + return 2; + } + return index; +} + +/** + * A list of preference observers that should be disabled whilst we write our + * preference values. + * + * @type {{ prefName: string, observer: object }[]} + */ +const prefObservers = []; + // __write_setting_to_prefs(settingIndex)__. // Take a given setting index and write the appropriate pref values // to the pref database. var write_setting_to_prefs = function (settingIndex) { - Object.keys(kSecuritySettings).forEach(prefName => - Services.prefs.setBoolPref( - prefName, - kSecuritySettings[prefName][settingIndex] - ) - ); + settingIndex = fixupIndex(settingIndex); + // Don't want to trigger our internal observers when setting ourselves. + for (const { prefName, observer } of prefObservers) { + Services.prefs.removeObserver(prefName, observer); + } + try { + // Make sure noscript is re-initialised at the next startup when the + // security level changes. + Services.prefs.setBoolPref(kNoScriptInitedPref, false); + Services.prefs.setIntPref(kSliderPref, settingIndex); + // NOTE: We do not clear kCustomPref. Instead, we rely on the preference + // being cleared on the next startup. + Object.keys(kSecuritySettings).forEach(prefName => + Services.prefs.setBoolPref( + prefName, + kSecuritySettings[prefName][settingIndex] + ) + ); + } finally { + // Re-add the observers. + for (const { prefName, observer } of prefObservers) { + Services.prefs.addObserver(prefName, observer); + } + } }; // __read_setting_from_prefs()__. @@ -309,24 +348,6 @@ var read_setting_from_prefs = function (prefNames) { return null; }; -// __watch_security_prefs(onSettingChanged)__. -// Whenever a pref bound to the security slider changes, onSettingChanged -// is called with the new security setting value (1,2,3,4 or null). -// Returns a zero-arg function that ends this binding. -var watch_security_prefs = function (onSettingChanged) { - let prefNames = Object.keys(kSecuritySettings); - let unbindFuncs = []; - for (let prefName of prefNames) { - unbindFuncs.push( - bindPrefAndInit(prefName, () => - onSettingChanged(read_setting_from_prefs()) - ) - ); - } - // Call all the unbind functions. - return () => unbindFuncs.forEach(unbind => unbind()); -}; - // __initialized__. // Have we called initialize() yet? var initializedSecPrefs = false; @@ -342,36 +363,82 @@ var initializeSecurityPrefs = function () { } logger.info("Initializing security-prefs.js"); initializedSecPrefs = true; - // When security_custom is set to false, apply security_slider setting - // to the security-sensitive prefs. - bindPrefAndInit(kCustomPref, function (custom) { - if (custom === false) { - write_setting_to_prefs(Services.prefs.getIntPref(kSliderPref)); - } - }); - // If security_slider is given a new value, then security_custom should - // be set to false. - bindPref(kSliderPref, function (prefIndex) { + + const wasCustom = Services.prefs.getBoolPref(kCustomPref, false); + // For new profiles with no user preference, the security level should be "4" + // and it should not be custom. + let desiredIndex = Services.prefs.getIntPref(kSliderPref, 4); + desiredIndex = fixupIndex(desiredIndex); + // Make sure the user has a set preference user value. + Services.prefs.setIntPref(kSliderPref, desiredIndex); + Services.prefs.setBoolPref(kCustomPref, wasCustom); + + // Make sure that the preference values at application startup match the + // expected values for the desired security level. See tor-browser#43783. + + // NOTE: We assume that the controlled preference values that are read prior + // to profile-after-change do not change in value before this method is + // called. I.e. we expect the current preference values to match the + // preference values that were used during the application initialisation. + const effectiveIndex = read_setting_from_prefs(); + + if (wasCustom && effectiveIndex !== null) { + logger.info(`Custom startup values match index ${effectiveIndex}`); + // Do not consider custom any more. + // NOTE: This level needs to be set before it is read elsewhere. In + // particular, for the NoScript addon. Services.prefs.setBoolPref(kCustomPref, false); - write_setting_to_prefs(prefIndex); + Services.prefs.setIntPref(kSliderPref, effectiveIndex); + } else if (!wasCustom && effectiveIndex !== desiredIndex) { + // NOTE: We assume all our controlled preferences require a restart. + // In practice, only a subset of these preferences may actually require a + // restart, so we could switch their values. But we treat them all the same + // for simplicity, consistency and stability in case mozilla changes the + // restart requirements. + logger.info(`Startup values do not match for index ${desiredIndex}`); + SecurityLevelPrefs.requireRestart(); + } + + // Start listening for external changes to the controlled preferences. + prefObservers.push({ + prefName: kCustomPref, + observer: bindPref(kCustomPref, custom => { + // Custom flag was removed mid-session. Requires a restart to apply the + // security level. + if (custom === false) { + logger.info("Custom flag was cleared externally"); + SecurityLevelPrefs.requireRestart(); + } + }), }); - // If a security-sensitive pref changes, then decide if the set of pref values - // constitutes a security_slider setting or a custom value. - watch_security_prefs(settingIndex => { - if (settingIndex === null) { - Services.prefs.setBoolPref(kCustomPref, true); - } else { - Services.prefs.setIntPref(kSliderPref, settingIndex); - Services.prefs.setBoolPref(kCustomPref, false); - } + prefObservers.push({ + prefName: kSliderPref, + observer: bindPref(kSliderPref, () => { + // Security level was changed mid-session. Requires a restart to apply. + logger.info("Security level was changed externally"); + SecurityLevelPrefs.requireRestart(); + }), }); - // Migrate from old medium-low (3) to new medium (2). - if ( - Services.prefs.getBoolPref(kCustomPref) === false && - Services.prefs.getIntPref(kSliderPref) === 3 - ) { - Services.prefs.setIntPref(kSliderPref, 2); - write_setting_to_prefs(2); + + for (const prefName of Object.keys(kSecuritySettings)) { + prefObservers.push({ + prefName, + observer: bindPref(prefName, () => { + logger.warn( + `The controlled preference ${prefName} was changed externally.` + + " Treating as a custom security level." + ); + // Something outside of this module changed the preference value for a + // preference we control. + // Always treat as a custom security level for the rest of this session, + // even if the new preference values match a pre-set security level. We + // do this because some controlled preferences require a restart to be + // properly applied. See tor-browser#43783. + // In the case where it does match a pre-set security level, the custom + // flag will be cleared at the next startup. + Services.prefs.setBoolPref(kCustomPref, true); + }), + }); } logger.info("security-prefs.js initialization complete"); @@ -425,8 +492,9 @@ export class SecurityLevel { init() { migratePreferences(); - initializeNoScriptControl(); + // Fixup our preferences before we pass on the security level to NoScript. initializeSecurityPrefs(); + initializeNoScriptControl(); } observe(aSubject, aTopic) { @@ -436,10 +504,19 @@ export class SecurityLevel { } } +/** + * @typedef {object} SecurityLevelRestartNotificationHandler + * + * An object that can serve the user a restart notification. + * + * @property {Function} tryRestartBrowser - The method that should be called to + * ask the user to restart the browser. + */ + /* Security Level Prefs - Getters and Setters for relevant torbutton prefs + Getters and Setters for relevant security level prefs */ export const SecurityLevelPrefs = { SecurityLevels: Object.freeze({ @@ -450,6 +527,14 @@ export const SecurityLevelPrefs = { security_slider_pref: "browser.security_level.security_slider", security_custom_pref: "browser.security_level.security_custom", + /** + * The current security level preference. + * + * This ignores any custom settings the user may have changed, and just + * gives the underlying security level. + * + * @type {?string} + */ get securityLevel() { // Set the default return value to 0, which won't match anything in // SecurityLevels. @@ -459,18 +544,146 @@ export const SecurityLevelPrefs = { )?.[0]; }, - set securityLevel(level) { - const val = this.SecurityLevels[level]; - if (val !== undefined) { - Services.prefs.setIntPref(this.security_slider_pref, val); - } + /** + * Set the desired security level just before a restart. + * + * The caller must restart the browser after calling this method. + * + * @param {string} level - The name of the new security level to set. + */ + setSecurityLevelBeforeRestart(level) { + write_setting_to_prefs(this.SecurityLevels[level]); }, + /** + * Whether the user has any custom setting values that do not match a pre-set + * security level. + * + * @type {boolean} + */ get securityCustom() { return Services.prefs.getBoolPref(this.security_custom_pref); }, - set securityCustom(val) { - Services.prefs.setBoolPref(this.security_custom_pref, val); + /** + * A summary of the current security level. + * + * If the user has some custom settings, this returns "custom". Otherwise + * returns the name of the security level. + * + * @type {string} + */ + get securityLevelSummary() { + if (this.securityCustom) { + return "custom"; + } + return this.securityLevel ?? "custom"; + }, + + /** + * Whether the browser should be restarted to apply the security level. + * + * @type {boolean} + */ + _needRestart: false, + + /** + * The external handler that can show a notification to the user, if any. + * + * @type {?SecurityLevelRestartNotificationHandler} + */ + _restartNotificationHandler: null, + + /** + * Set the external handler for showing notifications to the user. + * + * This should only be called once per session once the handler is ready to + * show a notification, which may occur immediately during this call. + * + * @param {SecurityLevelRestartNotificationHandler} handler - The new handler + * to use. + */ + setRestartNotificationHandler(handler) { + logger.info("Restart notification handler is set"); + this._restartNotificationHandler = handler; + if (this._needRestart) { + // Show now using the new handler. + this._tryShowRestartNotification(); + } + }, + + /** + * A promise for any ongoing notification prompt task. + * + * @type {Promise} + */ + _restartNotificationPromise: null, + + /** + * Try show a notification to the user. + * + * If no notification handler has been attached yet, this will do nothing. + */ + async _tryShowRestartNotification() { + if (!this._restartNotificationHandler) { + logger.info("Missing a restart notification handler"); + // This may be added later in the session. + return; + } + + const prevPromise = this._restartNotificationPromise; + let resolve; + ({ promise: this._restartNotificationPromise, resolve } = + Promise.withResolvers()); + await prevPromise; + + try { + await this._restartNotificationHandler?.tryRestartBrowser(); + } finally { + // Allow the notification to be shown again. + resolve(); + } + }, + + /** + * Mark the session as requiring a restart to apply a change in security + * level. + * + * The security level will immediately be switched to "custom", and the user + * may be shown a notification to restart the browser. + */ + requireRestart() { + logger.warn("The browser needs to be restarted to set the security level"); + // Treat as a custom security level for the rest of the session. + // At the next startup, the custom flag may be cleared if the settings are + // as expected. + Services.prefs.setBoolPref(kCustomPref, true); + this._needRestart = true; + + // NOTE: We need to change the controlled security level preferences in + // response to the desired change in security level. We could either: + // 1. Only change the controlled preferences after the user confirms a + // restart. Or + // 2. Change the controlled preferences and then try and ask the user to + // restart. + // + // We choose the latter: + // 1. To allow users to manually restart. + // 2. If the user ignores or misses the notification, they will at least be + // in the correct state when the browser starts again. Although they will + // be in a custom/undefined state in the mean time. + // 3. Currently Android relies on triggering the change in security level + // by setting the browser.security_level.security_slider preference + // value. So it currently uses this path. So we need to set the values + // now, before it preforms a restart. + // TODO: Have android use the `setSecurityLevelBeforeRestart` method + // instead of setting the security_slider preference value directly, so that + // it knows exactly when it can restart the browser. tor-browser#43820 + write_setting_to_prefs(Services.prefs.getIntPref(kSliderPref, 0)); + // NOTE: Even though we have written the preferences, the session should + // still be marked as "custom" because: + // 1. Some preferences require a browser restart to be applied. + // 2. NoScript has not been updated with the new settings. + this._tryShowRestartNotification(); }, }; /* Security Level Prefs */ ===================================== toolkit/locales/en-US/toolkit/global/base-browser.ftl ===================================== @@ -128,10 +128,6 @@ security-level-toolbar-button-custom = # Uses sentence case in English (US). security-level-panel-heading = Security level - -security-level-panel-level-standard = Standard -security-level-panel-level-safer = Safer -security-level-panel-level-safest = Safest security-level-panel-learn-more-link = Learn more # Button to open security level settings. security-level-panel-open-settings-button = Settings… @@ -141,6 +137,19 @@ security-level-panel-open-settings-button = Settings… security-level-preferences-heading = Security Level security-level-preferences-overview = Disable certain web features that can be used to attack your security and anonymity. security-level-preferences-learn-more-link = Learn more +# Text for a badge that labels the currently active security level. +# The text in between '<span>' and '</span>' should contain some kind of bracket, like '(' and ')', or other punctuation used in your language to separate out text from its surrounding context. This will not be visible, but will be use for screen readers to make it clear that the text is not part of the same sentence. For example, in US English this would be read as "(Current level)", and the full line of text would be read as "Safest (Current level)". +security-level-preferences-current-badge = <span>(</span>Current level<span>)</span> +security-level-preferences-change-button = Change… + +## Security level settings dialog. + +security-level-dialog-window = + .title = Change security level + +# '-brand-short-name' is the localized browser name, like "Tor Browser". +security-level-dialog-restart-description = You will need to restart { -brand-short-name } to apply any changes. This will close all windows and tabs. + security-level-preferences-level-standard = .label = Standard security-level-preferences-level-safer = @@ -148,6 +157,16 @@ security-level-preferences-level-safer = security-level-preferences-level-safest = .label = Safest +security-level-dialog-save-restart = + .label = Save and restart + +## Security level names shown in the security panel and settings. + +security-level-panel-level-standard = Standard +security-level-panel-level-safer = Safer +security-level-panel-level-safest = Safest +security-level-panel-level-custom = Custom + ## Security level summaries shown in security panel and settings. security-level-summary-standard = All browser and website features are enabled. @@ -166,13 +185,13 @@ security-level-preferences-bullet-limit-font-and-symbols-and-images = Some fonts ## Custom security level. ## Some custom preferences configuration has placed the user outside one of the standard three levels. -# Shown in the security level panel as an orange badge next to the expected level. -security-level-panel-custom-badge = Custom -# Shown in the security level settings in a warning box. -security-level-preferences-custom-heading = Custom security level configured # Description of custom state and recommended action. # Shown in the security level panel and settings. security-level-summary-custom = Your custom browser preferences have resulted in unusual security settings. For security and privacy reasons, we recommend you choose one of the default security levels. -# Button to undo custom changes to the security level and place the user in one of the standard security levels. -# Shown in the security level panel and settings. -security-level-restore-defaults-button = Restore defaults + +## Security level restart prompt. + +security-level-restart-prompt-title = Your security level settings require a restart +security-level-restart-prompt-body = You must restart { -brand-short-name } for your security level settings to be applied. This will close all your windows and tabs. +security-level-restart-prompt-button-restart = Restart +security-level-restart-prompt-button-ignore = Ignore ===================================== toolkit/modules/TorAndroidIntegration.sys.mjs ===================================== @@ -11,6 +11,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorSettings: "resource://gre/modules/TorSettings.sys.mjs", + SecurityLevelPrefs: "resources://gre/modules/SecurityLevel.sys.mjs", }); const Prefs = Object.freeze({ @@ -34,6 +35,8 @@ const EmittedEvents = Object.freeze({ }); const ListenedEvents = Object.freeze({ + securityLevelGet: "GeckoView:Tor:SecurityLevelGet", + securityLevelSetBeforeRestart: "GeckoView:Tor:SecurityLevelSetBeforeRestart", settingsGet: "GeckoView:Tor:SettingsGet", // The data is passed directly to TorSettings. settingsSet: "GeckoView:Tor:SettingsSet", @@ -171,6 +174,20 @@ class TorAndroidIntegrationImpl { logger.debug(`Received event ${event}`, data); try { switch (event) { + case ListenedEvents.securityLevelGet: + // "standard"/"safer"/"safest" + // TODO: Switch to securityLevelSummary to allow android to handle + // "custom" security level. tor-browser#43819 + callback?.onSuccess(lazy.SecurityLevelPrefs.securityLevel); + break; + case ListenedEvents.securityLevelSetBeforeRestart: + lazy.SecurityLevelPrefs.setSecurityLevelBeforeRestart(data.levelName); + // Let the caller know that the setting is applied and the browser + // should be restarted now. + // NOTE: The caller must wait for this callback before triggering + // the restart. + callback?.onSuccess(); + break; case ListenedEvents.settingsGet: callback?.onSuccess(lazy.TorSettings.getSettings()); return; View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/e8925f6... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/e8925f6... You're receiving this email because of your account on gitlab.torproject.org.
participants (1)
-
ma1 (@ma1)