tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 92e4663878cdb6eee04cfd7caca7e27b28d5dcaf
parent 535c330c194b90cc2de3b9bef3a167d5e32613f5
Author: Pier Angelo Vendrame <pierov@torproject.org>
Date:   Fri,  8 Jul 2022 16:19:41 +0200

BB 40925: Implemented the Security Level component

This component adds a new Security Level toolbar button which visually
indicates the current global security level via icon (as defined by the
extensions.torbutton.security_slider pref), a drop-down hanger with a
short description of the current security level, and a new section in
the about:preferences#privacy page where users can change their current
security level. In addition, the hanger and the preferences page will
show a visual warning when the user has modified prefs associated with
the security level and provide a one-click 'Restore Defaults' button to
get the user back on recommended settings.

Bug 40125: Expose Security Level pref in GeckoView

Diffstat:
Mbrowser/base/content/browser-init.js | 5+++++
Mbrowser/base/content/browser.js | 5+++++
Mbrowser/base/content/browser.js.globals | 3++-
Mbrowser/base/content/browser.xhtml | 3+++
Mbrowser/base/content/main-popupset.inc.xhtml | 1+
Mbrowser/base/content/navigator-toolbox.inc.xhtml | 2++
Mbrowser/components/BrowserComponents.manifest | 1+
Mbrowser/components/moz.build | 1+
Mbrowser/components/preferences/preferences.xhtml | 2++
Mbrowser/components/preferences/privacy.inc.xhtml | 2++
Mbrowser/components/preferences/privacy.js | 18++++++++++++++++++
Abrowser/components/securitylevel/SecurityLevelUIUtils.sys.mjs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevel.js | 363+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelButton.css | 19+++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelButton.inc.xhtml | 4++++
Abrowser/components/securitylevel/content/securityLevelDialog.js | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelDialog.xhtml | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelIcon.svg | 40++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelPanel.css | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelPanel.inc.xhtml | 34++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelPreferences.css | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/content/securityLevelPreferences.inc.xhtml | 35+++++++++++++++++++++++++++++++++++
Abrowser/components/securitylevel/jar.mn | 8++++++++
Abrowser/components/securitylevel/moz.build | 5+++++
Mbrowser/installer/package-manifest.in | 3+++
Abrowser/modules/SecurityLevelNotification.sys.mjs | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/modules/moz.build | 1+
Mmobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt | 31++++++++++++++++++++++++-------
Mmobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt | 4++--
Mmobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt | 7+++++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/UseCases.kt | 1+
Mmobile/android/geckoview/api.txt | 3+++
Mmobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java | 33+++++++++++++++++++++++++++++++++
Mmobile/android/installer/package-manifest.in | 3+++
Mtoolkit/components/moz.build | 1+
Mtoolkit/components/search/SearchEngineSelector.sys.mjs | 15+++++++++++++++
Mtoolkit/components/search/SearchService.sys.mjs | 12++++++++++++
Atoolkit/components/search/tests/xpcshell/test_security_level.js | 22++++++++++++++++++++++
Mtoolkit/components/search/tests/xpcshell/xpcshell.toml | 3+++
Atoolkit/components/securitylevel/SecurityLevel.manifest | 1+
Atoolkit/components/securitylevel/SecurityLevel.sys.mjs | 831+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atoolkit/components/securitylevel/components.conf | 10++++++++++
Atoolkit/components/securitylevel/moz.build | 11+++++++++++
43 files changed, 2278 insertions(+), 12 deletions(-)

diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js @@ -243,6 +243,9 @@ var gBrowserInit = { // doesn't flicker as the window is being shown. DownloadsButton.init(); + // Init the SecurityLevelButton + SecurityLevelButton.init(); + // Certain kinds of automigration rely on this notification to complete // their tasks BEFORE the browser window is shown. SessionStore uses it to // restore tabs into windows AFTER important parts like gMultiProcessBrowser @@ -1093,6 +1096,8 @@ var gBrowserInit = { DownloadsButton.uninit(); + SecurityLevelButton.uninit(); + if (gToolbarKeyNavEnabled) { ToolbarKeyboardNavigator.uninit(); } diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -268,6 +268,11 @@ XPCOMUtils.defineLazyScriptGetter( ); XPCOMUtils.defineLazyScriptGetter( this, + ["SecurityLevelButton"], + "chrome://browser/content/securitylevel/securityLevel.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, "gEditItemOverlay", "chrome://browser/content/places/editBookmark.js" ); diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals @@ -238,5 +238,6 @@ "ActionsProviderContextualSearch", "ToolbarDropHandler", "ProfilesDatastoreService", - "gTrustPanelHandler" + "gTrustPanelHandler", + "SecurityLevelButton" ] diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml @@ -55,6 +55,9 @@ href="chrome://global/content/resistfingerprinting/letterboxing.css" /> + <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelPanel.css" /> + <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelButton.css" /> + <link rel="localization" href="branding/brand.ftl"/> <link rel="localization" href="browser/allTabsMenu.ftl"/> <link rel="localization" href="browser/appmenu.ftl"/> diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml @@ -688,6 +688,7 @@ #include ../../components/translations/content/selectTranslationsPanel.inc.xhtml #include ../../components/translations/content/fullPageTranslationsPanel.inc.xhtml #include ../../components/tabbrowser/content/browser-allTabsMenu.inc.xhtml +#include ../../components/securitylevel/content/securityLevelPanel.inc.xhtml <tooltip id="dynamic-shortcut-tooltip"/> diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml @@ -451,6 +451,8 @@ </box> </toolbarbutton> +#include ../../components/securitylevel/content/securityLevelButton.inc.xhtml + <toolbarbutton id="fxa-toolbar-menu-button" class="toolbarbutton-1 chromeclass-toolbar-additional subviewbutton-nav" badged="true" delegatesanchor="true" diff --git a/browser/components/BrowserComponents.manifest b/browser/components/BrowserComponents.manifest @@ -59,6 +59,7 @@ category browser-window-delayed-startup resource://gre/modules/SandboxUtils.sys. category browser-first-window-ready resource://gre/modules/SandboxUtils.sys.mjs SandboxUtils.observeContentSandboxPref #endif category browser-first-window-ready moz-src:///browser/modules/ClipboardPrivacy.sys.mjs ClipboardPrivacy.init +category browser-first-window-ready moz-src:///browser/modules/SecurityLevelNotification.sys.mjs SecurityLevelNotification.ready category browser-idle-startup moz-src:///browser/components/places/PlacesUIUtils.sys.mjs PlacesUIUtils.unblockToolbars category browser-idle-startup resource:///modules/BuiltInThemes.sys.mjs BuiltInThemes.ensureBuiltInThemes diff --git a/browser/components/moz.build b/browser/components/moz.build @@ -62,6 +62,7 @@ DIRS += [ "screenshots", "search", "security", + "securitylevel", "sessionstore", "shell", "sidebar", diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml @@ -46,6 +46,8 @@ href="chrome://browser/content/preferences/letterboxing.css" /> + <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelPreferences.css" /> + <link rel="localization" href="branding/brand.ftl"/> <link rel="localization" href="browser/browser.ftl"/> <!-- Used by fontbuilder.js --> diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml @@ -725,6 +725,8 @@ <html:h1 data-l10n-id="security-header"/> </hbox> +#include ../securitylevel/content/securityLevelPreferences.inc.xhtml + <!-- addons, forgery (phishing) UI Security --> <groupbox data-category="panePrivacy" hidden="true"> <label><html:h2 data-l10n-id="security-browsing-protection" class="section-heading"/></label> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js @@ -62,6 +62,13 @@ ChromeUtils.defineLazyGetter(lazy, "gParentalControlsService", () => : null ); +// TODO: module import via ChromeUtils.defineModuleGetter +XPCOMUtils.defineLazyScriptGetter( + this, + ["SecurityLevelPreferences"], + "chrome://browser/content/securitylevel/securityLevel.js" +); + XPCOMUtils.defineLazyServiceGetter( lazy, "TrackingDBService", @@ -3361,6 +3368,16 @@ var gPrivacyPane = { _pane: null, /** + * Show the Security Level UI + */ + _initSecurityLevel() { + SecurityLevelPreferences.init(); + window.addEventListener("unload", () => SecurityLevelPreferences.uninit(), { + once: true, + }); + }, + + /** * Whether the prompt to restart Firefox should appear when changing the autostart pref. */ _shouldPromptForRestart: true, @@ -3860,6 +3877,7 @@ var gPrivacyPane = { this._initTrackingProtectionExtensionControl(); this._ensureTrackingProtectionExceptionListMigration(); this._initProfilesInfo(); + this._initSecurityLevel(); Preferences.get("privacy.trackingprotection.enabled").on( "change", diff --git a/browser/components/securitylevel/SecurityLevelUIUtils.sys.mjs b/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; + }, +}; diff --git a/browser/components/securitylevel/content/securityLevel.js b/browser/components/securitylevel/content/securityLevel.js @@ -0,0 +1,363 @@ +"use strict"; + +/* global AppConstants, Services, openPreferences, XPCOMUtils, gSubDialog */ + +ChromeUtils.defineESModuleGetters(this, { + SecurityLevelPrefs: "resource://gre/modules/SecurityLevel.sys.mjs", + SecurityLevelUIUtils: "resource:///modules/SecurityLevelUIUtils.sys.mjs", +}); + +/* + Security Level Button Code + + Controls init and update of the security level toolbar button +*/ + +var SecurityLevelButton = { + _securityPrefsBranch: null, + /** + * Whether we have added popup listeners to the panel. + * + * @type {boolean} + */ + _panelPopupListenersSetup: false, + /** + * The toolbar button element. + * + * @type {Element} + */ + _button: null, + /** + * The button that the panel should point to. Either the toolbar button or the + * overflow button. + * + * @type {Element} + */ + _anchorButton: null, + + _configUIFromPrefs() { + const level = SecurityLevelPrefs.securityLevelSummary; + this._button.setAttribute("level", level); + + let l10nIdLevel; + switch (level) { + case "standard": + l10nIdLevel = "security-level-toolbar-button-standard"; + break; + case "safer": + l10nIdLevel = "security-level-toolbar-button-safer"; + break; + case "safest": + l10nIdLevel = "security-level-toolbar-button-safest"; + break; + case "custom": + l10nIdLevel = "security-level-toolbar-button-custom"; + break; + default: + throw Error(`Unhandled level: ${level}`); + } + document.l10n.setAttributes(this._button, l10nIdLevel); + }, + + /** + * Open the panel popup for the button. + */ + openPopup() { + const overflowPanel = document.getElementById("widget-overflow"); + if (overflowPanel.contains(this._button)) { + // We are in the overflow panel. + // We first close the overflow panel, otherwise focus will not return to + // the nav-bar-overflow-button if the security level panel is closed with + // "Escape" (the navigation toolbar does not track focus when a panel is + // opened whilst another is already open). + // NOTE: In principle, using PanelMultiView would allow us to open panels + // from within another panel. However, when using panelmultiview for the + // security level panel, tab navigation was broken within the security + // level panel. PanelMultiView may be set up to work with a menu-like + // panel rather than our dialog-like panel. + overflowPanel.hidePopup(); + this._anchorButton = document.getElementById("nav-bar-overflow-button"); + } else { + this._anchorButton = this._button; + } + + const panel = SecurityLevelPanel.panel; + if (!this._panelPopupListenersSetup) { + this._panelPopupListenersSetup = true; + // NOTE: We expect the _anchorButton to not change whilst the popup is + // open. + panel.addEventListener("popupshown", () => { + this._anchorButton.setAttribute("open", "true"); + }); + panel.addEventListener("popuphidden", () => { + this._anchorButton.removeAttribute("open"); + }); + } + + panel.openPopup( + this._anchorButton.icon, + "bottomright topright", + 0, + 0, + false + ); + }, + + init() { + // We first search in the DOM for the security level button. If it does not + // exist it may be in the toolbox palette. We still want to return the + // button in the latter case to allow it to be initialized or adjusted in + // case it is added back through customization. + this._button = + document.getElementById("security-level-button") || + window.gNavToolbox.palette.querySelector("#security-level-button"); + // Set a label to be be used as the accessible name, and to be shown in the + // overflow menu and during customization. + this._button.addEventListener("command", () => this.openPopup()); + // set the initial class based off of the current pref + this._configUIFromPrefs(); + + this._securityPrefsBranch = Services.prefs.getBranch( + "browser.security_level." + ); + this._securityPrefsBranch.addObserver("", this); + + SecurityLevelPanel.init(); + }, + + uninit() { + this._securityPrefsBranch.removeObserver("", this); + this._securityPrefsBranch = null; + + SecurityLevelPanel.uninit(); + }, + + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + if (data === "security_slider" || data === "security_custom") { + this._configUIFromPrefs(); + } + break; + } + }, +}; /* SecurityLevelButton */ + +/* + Security Level Panel Code + + Controls init and update of the panel in the security level hanger +*/ + +var SecurityLevelPanel = { + _securityPrefsBranch: null, + _populated: false, + + _populateXUL() { + this._elements = { + panel: document.getElementById("securityLevel-panel"), + levelName: document.getElementById("securityLevel-level"), + summary: document.getElementById("securityLevel-summary"), + }; + + const learnMoreEl = document.getElementById("securityLevel-learnMore"); + learnMoreEl.addEventListener("click", () => { + this.hide(); + }); + + document + .getElementById("securityLevel-settings") + .addEventListener("command", () => { + this.openSecuritySettings(); + }); + + this._elements.panel.addEventListener("popupshown", () => { + // Bring focus into the panel by focusing the default button. + this._elements.panel.querySelector('button[default="true"]').focus(); + }); + + this._populated = true; + this._configUIFromPrefs(); + }, + + _configUIFromPrefs() { + if (!this._populated) { + return; + } + + // get security prefs + const level = SecurityLevelPrefs.securityLevelSummary; + + // Descriptions change based on security level + this._elements.panel.setAttribute("level", level); + let l10nIdLevel; + let l10nIdSummary; + switch (level) { + case "standard": + l10nIdLevel = "security-level-panel-level-standard"; + l10nIdSummary = "security-level-summary-standard"; + break; + case "safer": + l10nIdLevel = "security-level-panel-level-safer"; + l10nIdSummary = "security-level-summary-safer"; + break; + case "safest": + 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}`); + } + + document.l10n.setAttributes(this._elements.levelName, l10nIdLevel); + document.l10n.setAttributes(this._elements.summary, l10nIdSummary); + }, + + /** + * The popup element. + * + * @type {MozPanel} + */ + get panel() { + if (!this._populated) { + this._populateXUL(); + } + return this._elements.panel; + }, + + init() { + this._securityPrefsBranch = Services.prefs.getBranch( + "browser.security_level." + ); + this._securityPrefsBranch.addObserver("", this); + }, + + uninit() { + this._securityPrefsBranch.removeObserver("", this); + this._securityPrefsBranch = null; + }, + + hide() { + this._elements.panel.hidePopup(); + }, + + openSecuritySettings() { + openPreferences("privacy-securitylevel"); + this.hide(); + }, + + // callback when prefs change + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + if (data == "security_slider" || data == "security_custom") { + this._configUIFromPrefs(); + } + break; + } + }, +}; /* SecurityLevelPanel */ + +/* + Security Level Preferences Code + + Code to handle init and update of security level section in about:preferences#privacy +*/ + +var SecurityLevelPreferences = { + _securityPrefsBranch: null, + + /** + * The element that shows the current security level. + * + * @type {?Element} + */ + _currentEl: null, + + _populateXUL() { + this._currentEl = document.getElementById("security-level-current"); + const changeButton = document.getElementById("security-level-change"); + const badgeEl = this._currentEl.querySelector( + ".security-level-current-badge" + ); + + 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); + } + + changeButton.addEventListener("click", () => { + this._openDialog(); + }); + }, + + _openDialog() { + gSubDialog.open( + "chrome://browser/content/securitylevel/securityLevelDialog.xhtml", + { features: "resizable=yes" } + ); + }, + + _configUIFromPrefs() { + // Set a data-current-level attribute for showing the current security + // level, and hiding the rest. + this._currentEl.dataset.currentLevel = + SecurityLevelPrefs.securityLevelSummary; + }, + + init() { + // populate XUL with localized strings + this._populateXUL(); + + // read prefs and populate UI + this._configUIFromPrefs(); + + // register for pref chagnes + this._securityPrefsBranch = Services.prefs.getBranch( + "browser.security_level." + ); + this._securityPrefsBranch.addObserver("", this); + }, + + uninit() { + // unregister for pref change events + this._securityPrefsBranch.removeObserver("", this); + this._securityPrefsBranch = null; + }, + + // callback for when prefs change + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + if (data == "security_slider" || data == "security_custom") { + this._configUIFromPrefs(); + } + break; + } + }, +}; /* SecurityLevelPreferences */ diff --git a/browser/components/securitylevel/content/securityLevelButton.css b/browser/components/securitylevel/content/securityLevelButton.css @@ -0,0 +1,19 @@ +#security-level-button[level="standard"] { + list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard"); +} + +#security-level-button[level="safer"] { + list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safer"); +} + +#security-level-button[level="safest"] { + list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest"); +} + +#security-level-button[level="custom"] { + list-style-image: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard_custom"); +} + +#security-level-button:-moz-locale-dir(rtl) .toolbarbutton-icon { + transform: scaleX(-1); +} diff --git a/browser/components/securitylevel/content/securityLevelButton.inc.xhtml b/browser/components/securitylevel/content/securityLevelButton.inc.xhtml @@ -0,0 +1,4 @@ +<toolbarbutton id="security-level-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + removable="true" + cui-areatype="toolbar"/> diff --git a/browser/components/securitylevel/content/securityLevelDialog.js b/browser/components/securitylevel/content/securityLevelDialog.js @@ -0,0 +1,222 @@ +"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 => { + event.preventDefault(); + if (this._acceptButton.disabled) { + 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. + */ + async _commitChange() { + const doNotWarnPref = "browser.security_level.disable_warn_before_restart"; + if (!Services.prefs.getBoolPref(doNotWarnPref, false)) { + const [titleString, bodyString, checkboxString, restartString] = + await document.l10n.formatValues([ + { id: "security-level-restart-warning-dialog-title" }, + { id: "security-level-restart-warning-dialog-body" }, + { id: "restart-warning-dialog-do-not-warn-checkbox" }, + { id: "restart-warning-dialog-restart-button" }, + ]); + const flags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_0_DEFAULT + + Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL; + const propBag = await Services.prompt.asyncConfirmEx( + window.browsingContext.top, + Services.prompt.MODAL_TYPE_CONTENT, + titleString, + bodyString, + flags, + restartString, + null, + null, + checkboxString, + false, + { useTitle: true, noIcon: true } + ); + if (propBag.get("buttonNumClicked") !== 0) { + return; + } + if (propBag.get("checked")) { + Services.prefs.setBoolPref(doNotWarnPref, true); + } + } + 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 } + ); +}); diff --git a/browser/components/securitylevel/content/securityLevelDialog.xhtml b/browser/components/securitylevel/content/securityLevelDialog.xhtml @@ -0,0 +1,88 @@ +<?xml version="1.0"?> + +<?csp default-src chrome: ?> + +<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> diff --git a/browser/components/securitylevel/content/securityLevelIcon.svg b/browser/components/securitylevel/content/securityLevelIcon.svg @@ -0,0 +1,40 @@ +<svg width="16" height="16" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <style> + use:not(:target) { + display: none; + } + </style> + <defs> + <g id="standard_icon" stroke="none" stroke-width="1"> + <path clip-rule="evenodd" d="m8.49614.283505c-.30743-.175675-.68485-.175675-.99228.000001l-6 3.428574c-.31157.17804-.50386.50938-.50386.86824v1.41968c0 4 2.98667 9.0836 7 10 4.0133-.9164 7-6 7-10v-1.41968c0-.35886-.1923-.6902-.5039-.86824zm-.49614 1.216495-5.75 3.28571v1.2746c0 1.71749.65238 3.7522 1.78726 5.46629 1.07287 1.6204 2.47498 2.8062 3.96274 3.2425 1.48776-.4363 2.8899-1.6221 3.9627-3.2425 1.1349-1.71409 1.7873-3.7488 1.7873-5.46629v-1.2746z" fill-rule="evenodd" /> + </g> + <g id="safer_icon" stroke="none" stroke-width="1"> + <path clip-rule="evenodd" d="m8.49614.283505c-.30743-.175675-.68485-.175675-.99228.000001l-6 3.428574c-.31157.17804-.50386.50938-.50386.86824v1.41968c0 4 2.98667 9.0836 7 10 4.0133-.9164 7-6 7-10v-1.41968c0-.35886-.1923-.6902-.5039-.86824zm-.49614 1.216495-5.75 3.28571v1.2746c0 1.71749.65238 3.7522 1.78726 5.46629 1.07287 1.6204 2.47498 2.8062 3.96274 3.2425 1.48776-.4363 2.8899-1.6221 3.9627-3.2425 1.1349-1.71409 1.7873-3.7488 1.7873-5.46629v-1.2746z" fill-rule="evenodd"/> + <path d="m3.5 6.12062v-.40411c0-.08972.04807-.17255.12597-.21706l4-2.28572c.16666-.09523.37403.02511.37403.21707v10.0766c-1.01204-.408-2.054-1.3018-2.92048-2.6105-1.02134-1.54265-1.57952-3.34117-1.57952-4.77628z"/> + </g> + <g id="safest_icon" stroke="none" stroke-width="1"> + <path clip-rule="evenodd" d="m8.49614.283505c-.30743-.175675-.68485-.175675-.99228.000001l-6 3.428574c-.31157.17804-.50386.50938-.50386.86824v1.41968c0 4 2.98667 9.0836 7 10 4.0133-.9164 7-6 7-10v-1.41968c0-.35886-.1923-.6902-.5039-.86824zm-.49614 1.216495-5.75 3.28571v1.2746c0 1.71749.65238 3.7522 1.78726 5.46629 1.07287 1.6204 2.47498 2.8062 3.96274 3.2425 1.48776-.4363 2.8899-1.6221 3.9627-3.2425 1.1349-1.71409 1.7873-3.7488 1.7873-5.46629v-1.2746z" fill-rule="evenodd"/> + <path d="m3.5 6.12062v-.40411c0-.08972.04807-.17255.12597-.21706l4.25-2.42857c.07685-.04392.17121-.04392.24806 0l4.24997 2.42857c.0779.04451.126.12734.126.21706v.40411c0 1.43511-.5582 3.23363-1.5795 4.77628-.8665 1.3087-1.90846 2.2025-2.9205 2.6105-1.01204-.408-2.054-1.3018-2.92048-2.6105-1.02134-1.54265-1.57952-3.34117-1.57952-4.77628z"/> + </g> + <g id="standard_custom_icon" stroke="none" stroke-width="1"> + <path d="m9.37255.784312-.87641-.500806c-.30743-.175676-.68485-.175676-.99228 0l-6 3.428574c-.31157.17804-.50386.50938-.50386.86824v1.41968c0 4 2.98667 9.0836 7 10 3.7599-.8585 6.6186-5.3745 6.9647-9.23043-.4008.20936-.8392.35666-1.3024.42914-.2132 1.43414-.8072 2.98009-1.6996 4.32789-1.0728 1.6204-2.47494 2.8062-3.9627 3.2425-1.48776-.4363-2.88987-1.6221-3.96274-3.2425-1.13488-1.71409-1.78726-3.7488-1.78726-5.46629v-1.2746l5.75-3.28571.86913.49664c.10502-.43392.27664-.84184.50342-1.212328z"/> + <circle cx="13" cy="3" fill="#ffbd2e" r="3"/> + </g> + <g id="safer_custom_icon" stroke="none" stroke-width="1"> + <path d="m9.37255.784312-.87641-.500806c-.30743-.175676-.68485-.175676-.99228 0l-6 3.428574c-.31157.17804-.50386.50938-.50386.86824v1.41968c0 4 2.98667 9.0836 7 10 3.7599-.8585 6.6186-5.3745 6.9647-9.23043-.4008.20936-.8392.35666-1.3024.42914-.2132 1.43414-.8072 2.98009-1.6996 4.32789-1.0728 1.6204-2.47494 2.8062-3.9627 3.2425-1.48776-.4363-2.88987-1.6221-3.96274-3.2425-1.13488-1.71409-1.78726-3.7488-1.78726-5.46629v-1.2746l5.75-3.28571.86913.49664c.10502-.43392.27664-.84184.50342-1.212328z"/> + <path d="m3.5 6.12062v-.40411c0-.08972.04807-.17255.12597-.21706l4-2.28572c.16666-.09523.37403.02511.37403.21707v10.0766c-1.01204-.408-2.054-1.3018-2.92048-2.6105-1.02134-1.54265-1.57952-3.34117-1.57952-4.77628z"/> + <circle cx="13" cy="3" fill="#ffbd2e" r="3"/> + </g> + <g id="safest_custom_icon" stroke="none" stroke-width="1"> + <path d="m9.37255.784312-.87641-.500806c-.30743-.175676-.68485-.175676-.99228 0l-6 3.428574c-.31157.17804-.50386.50938-.50386.86824v1.41968c0 4 2.98667 9.0836 7 10 3.7599-.8585 6.6186-5.3745 6.9647-9.23043-.4008.20936-.8392.35666-1.3024.42914-.2132 1.43414-.8072 2.98009-1.6996 4.32789-1.0728 1.6204-2.47494 2.8062-3.9627 3.2425-1.48776-.4363-2.88987-1.6221-3.96274-3.2425-1.13488-1.71409-1.78726-3.7488-1.78726-5.46629v-1.2746l5.75-3.28571.86913.49664c.10502-.43392.27664-.84184.50342-1.212328z"/> + <path d="m8.77266 3.44151-.64863-.37064c-.07685-.04392-.17121-.04392-.24806 0l-4.25 2.42857c-.0779.04451-.12597.12735-.12597.21706v.40412c0 1.4351.55818 3.23362 1.57952 4.77618.86648 1.3087 1.90844 2.2026 2.92048 2.6106 1.01204-.408 2.054-1.3018 2.9205-2.6106.7761-1.17217 1.2847-2.49215 1.4843-3.68816-1.9219-.26934-3.43158-1.82403-3.63214-3.76713z"/> + <circle cx="13" cy="3" fill="#ffbd2e" r="3"/> + </g> + </defs> + <use id="standard" fill="context-fill" fill-opacity="context-fill-opacity" href="#standard_icon" /> + <use id="safer" fill="context-fill" fill-opacity="context-fill-opacity" href="#safer_icon" /> + <use id="safest" fill="context-fill" fill-opacity="context-fill-opacity" href="#safest_icon" /> + <use id="standard_custom" fill="context-fill" fill-opacity="context-fill-opacity" href="#standard_custom_icon" /> + <use id="safer_custom" fill="context-fill" fill-opacity="context-fill-opacity" href="#safer_custom_icon" /> + <use id="safest_custom" fill="context-fill" fill-opacity="context-fill-opacity" href="#safest_custom_icon" /> +</svg> diff --git a/browser/components/securitylevel/content/securityLevelPanel.css b/browser/components/securitylevel/content/securityLevelPanel.css @@ -0,0 +1,75 @@ +/* Security Level CSS */ + +#securityLevel-background { + min-height: 10em; + padding-inline: 16px; + column-gap: 0.5em; + display: grid; + grid-template: + "top-pad icon" 16px + "title icon" auto + "body icon" auto + "learn-more icon" auto + "bottom-pad icon" minmax(8px, 1fr) + / auto auto; +} + +#securityLevel-background-image { + grid-area: icon; + --security-level-icon-size: 9em; + width: var(--security-level-icon-size); + height: var(--security-level-icon-size); + margin-block: 0.4em; + /* Middle of shield aligns with the panel padding: */ + margin-inline-end: calc(-0.5 * var(--security-level-icon-size)); + align-self: start; + justify-self: end; + /* This icon is meant to act as background, so disable dragging or interfering + * with clicks. */ + pointer-events: none; + -moz-context-properties: fill, fill-opacity; + fill-opacity: 1; + fill: var(--border-color-card); +} + +/* NOTE: Use ":dir" instead of ":-moz-locale-dir" when panel switches to HTML. */ +#securityLevel-background-image:-moz-locale-dir(rtl) { + transform: scaleX(-1); +} + +#securityLevel-panel:is([level="standard"], [level="custom"]) #securityLevel-background-image { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#standard"); +} + +#securityLevel-panel[level="safer"] #securityLevel-background-image { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safer"); +} + +#securityLevel-panel[level="safest"] #securityLevel-background-image { + content: url("chrome://browser/content/securitylevel/securityLevelIcon.svg#safest"); +} + +#securityLevel-background p { + margin-block: 0 16px; +} + +/* Override margin in panelUI-shared.css */ +#securityLevel-panel toolbarseparator#securityLevel-separator { + margin-inline: 16px; +} + +#securityLevel-level { + font-size: larger; + font-weight: var(--font-weight-bold); + grid-area: title; +} + +#securityLevel-summary { + max-width: 20em; + grid-area: body; +} + +#securityLevel-learnMore { + align-self: start; + grid-area: learn-more; +} diff --git a/browser/components/securitylevel/content/securityLevelPanel.inc.xhtml b/browser/components/securitylevel/content/securityLevelPanel.inc.xhtml @@ -0,0 +1,34 @@ +<panel id="securityLevel-panel" + role="dialog" + aria-labelledby="securityLevel-header" + aria-describedby="securityLevel-level securityLevel-summary" + type="arrow" + orient="vertical" + class="cui-widget-panel panel-no-padding"> + <box class="panel-header"> + <html:h1 + id="securityLevel-header" + data-l10n-id="security-level-panel-heading" + ></html:h1> + </box> + <toolbarseparator id="securityLevel-separator"></toolbarseparator> + <vbox id="securityLevel-background" class="panel-subview-body"> + <html:p id="securityLevel-level"></html:p> + <html:p id="securityLevel-summary"></html:p> + <html:a + is="moz-support-link" + id="securityLevel-learnMore" + tor-manual-page="security-settings" + data-l10n-id="security-level-panel-learn-more-link" + ></html:a> + <html:img id="securityLevel-background-image" alt="" /> + </vbox> + <html:moz-button-group class="panel-footer"> + <button + id="securityLevel-settings" + class="footer-button" + default="true" + data-l10n-id="security-level-panel-open-settings-button" + /> + </html:moz-button-group> +</panel> diff --git a/browser/components/securitylevel/content/securityLevelPreferences.css b/browser/components/securitylevel/content/securityLevelPreferences.css @@ -0,0 +1,170 @@ +.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; +} + +.security-level-icon { + grid-area: icon; + align-self: start; + width: 24px; + height: 24px; + -moz-context-properties: fill; + fill: var(--icon-color); + margin-block-start: var(--space-xsmall); + margin-inline-end: var(--space-large); +} + +:-moz-locale-dir(rtl) .security-level-icon { + transform: scaleX(-1); +} + +.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); +} + +.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: var(--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(--background-color-box); + border: 1px solid var(--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; +} + +#security-level-radiogroup .radio-label-box { + /* .security-level-current-badge already has a margin. */ + margin: 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; +} diff --git a/browser/components/securitylevel/content/securityLevelPreferences.inc.xhtml b/browser/components/securitylevel/content/securityLevelPreferences.inc.xhtml @@ -0,0 +1,35 @@ +<groupbox id="securityLevel-groupbox" + data-category="panePrivacy" + data-subcategory="securitylevel" + hidden="true"> + <label> + <html:h2 data-l10n-id="security-level-preferences-heading"></html:h2> + </label> + <vbox flex="1"> + <description class="description-deemphasized" flex="1"> + <html:span + id="securityLevel-overview" + data-l10n-id="security-level-preferences-overview" + ></html:span> + <html:a + is="moz-support-link" + tor-manual-page="security-settings" + data-l10n-id="security-level-preferences-learn-more-link" + ></html:a> + </description> + <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> diff --git a/browser/components/securitylevel/jar.mn b/browser/components/securitylevel/jar.mn @@ -0,0 +1,8 @@ +browser.jar: + content/browser/securitylevel/securityLevel.js (content/securityLevel.js) + content/browser/securitylevel/securityLevelPanel.css (content/securityLevelPanel.css) + 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) diff --git a/browser/components/securitylevel/moz.build b/browser/components/securitylevel/moz.build @@ -0,0 +1,5 @@ +JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES += [ + "SecurityLevelUIUtils.sys.mjs", +] diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in @@ -227,6 +227,9 @@ @RESPATH@/browser/chrome/icons/default/default128.png #endif +; Base Browser +@RESPATH@/components/SecurityLevel.manifest + ; [DevTools Startup Files] @RESPATH@/browser/chrome/devtools-startup@JAREXT@ @RESPATH@/browser/chrome/devtools-startup.manifest diff --git a/browser/modules/SecurityLevelNotification.sys.mjs b/browser/modules/SecurityLevelNotification.sys.mjs @@ -0,0 +1,119 @@ +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 SecurityLevelNotification = { + /** + * 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.setNotificationHandler(this); + }, + + /** + * Show the restart notification, and perform the restart if the user agrees. + * + * @returns {boolean} - Whether we are restarting the browser. + */ + async tryRestartBrowser() { + const [titleText, bodyText, primaryButtonText, secondaryButtonText] = + await lazy.NotificationStrings.formatValues([ + { id: "security-level-restart-prompt-title" }, + { id: "security-level-restart-prompt-body" }, + { id: "restart-warning-dialog-restart-button" }, + { id: "security-level-restart-prompt-button-ignore" }, + ]); + const buttonFlags = + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + + Services.prompt.BUTTON_POS_0_DEFAULT + + Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE + + 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 + ); + return true; + } + return false; + }, + + /** + * Show or re-show the custom security notification. + * + * @param {Function} userDismissedCallback - The callback for when the user + * dismisses the notification. + */ + async showCustomWarning(userDismissedCallback) { + const win = lazy.BrowserWindowTracker.getTopWindow(); + if (!win) { + return; + } + const typeName = "security-level-custom"; + const existing = win.gNotificationBox.getNotificationWithValue(typeName); + if (existing) { + win.gNotificationBox.removeNotification(existing); + } + + const buttons = [ + { + "l10n-id": "security-level-panel-open-settings-button", + callback() { + win.openPreferences("privacy-securitylevel"); + }, + }, + ]; + + win.gNotificationBox.appendNotification( + typeName, + { + label: { "l10n-id": "security-level-summary-custom" }, + priority: win.gNotificationBox.PRIORITY_WARNING_HIGH, + eventCallback: event => { + if (event === "dismissed") { + userDismissedCallback(); + } + }, + }, + buttons + ); + }, +}; diff --git a/browser/modules/moz.build b/browser/modules/moz.build @@ -153,6 +153,7 @@ MOZ_SRC_FILES += [ "FaviconUtils.sys.mjs", "ObserverForwarder.sys.mjs", "PrivateBrowsingUI.sys.mjs", + "SecurityLevelNotification.sys.mjs", "UnexpectedScriptObserver.sys.mjs", "WebAuthnPromptHelper.sys.mjs", ] diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt @@ -13,6 +13,8 @@ import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.Settings +import mozilla.components.concept.engine.UnsupportedSettingException import mozilla.components.feature.search.ext.buildSearchUrl import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.tabs.TabsUseCases @@ -25,6 +27,7 @@ class SearchUseCases( store: BrowserStore, tabsUseCases: TabsUseCases, sessionUseCases: SessionUseCases, + settings: Settings? = null, ) { interface SearchUseCase { /** @@ -41,6 +44,7 @@ class SearchUseCases( private val store: BrowserStore, private val tabsUseCases: TabsUseCases, private val sessionUseCases: SessionUseCases, + private val settings: Settings? = null, ) : SearchUseCase { private val logger = Logger("DefaultSearchUseCase") @@ -72,9 +76,15 @@ class SearchUseCases( flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), additionalHeaders: Map<String, String>? = null, ) { + var securityLevel: Int + try { + securityLevel = settings?.torSecurityLevel ?: 0 + } catch (e: UnsupportedSettingException) { + securityLevel = 0 + } val searchUrl = searchEngine?.let { - searchEngine.buildSearchUrl(searchTerms) - } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms) + searchEngine.buildSearchUrl(searchTerms, securityLevel) + } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms, securityLevel) if (searchUrl == null) { logger.warn("No default search engine available to perform search") @@ -124,6 +134,7 @@ class SearchUseCases( private val store: BrowserStore, private val tabsUseCases: TabsUseCases, private val isPrivate: Boolean, + private val settings: Settings? = null, ) : SearchUseCase { private val logger = Logger("NewTabSearchUseCase") @@ -161,9 +172,15 @@ class SearchUseCases( flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), additionalHeaders: Map<String, String>? = null, ) { + var securityLevel: Int + try { + securityLevel = settings?.torSecurityLevel ?: 0 + } catch (e: UnsupportedSettingException) { + securityLevel = 0 + } val searchUrl = searchEngine?.let { - searchEngine.buildSearchUrl(searchTerms) - } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms) + searchEngine.buildSearchUrl(searchTerms, securityLevel) + } ?: store.state.search.selectedOrDefaultSearchEngine?.buildSearchUrl(searchTerms, securityLevel) if (searchUrl == null) { logger.warn("No default search engine available to perform search") @@ -310,15 +327,15 @@ class SearchUseCases( } val defaultSearch: DefaultSearchUseCase by lazy { - DefaultSearchUseCase(store, tabsUseCases, sessionUseCases) + DefaultSearchUseCase(store, tabsUseCases, sessionUseCases, settings) } val newTabSearch: NewTabSearchUseCase by lazy { - NewTabSearchUseCase(store, tabsUseCases, false) + NewTabSearchUseCase(store, tabsUseCases, false, settings) } val newPrivateTabSearch: NewTabSearchUseCase by lazy { - NewTabSearchUseCase(store, tabsUseCases, true) + NewTabSearchUseCase(store, tabsUseCases, true, settings) } val addSearchEngine: AddNewSearchEngineUseCase by lazy { diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt @@ -105,9 +105,9 @@ fun SearchEngine.buildTrendingURL(): String? { /** * Builds a URL to search for the given search terms with this search engine. */ -fun SearchEngine.buildSearchUrl(searchTerm: String): String { +fun SearchEngine.buildSearchUrl(searchTerm: String, securityLevel: Int = 0): String { val builder = SearchUrlBuilder(this) - return builder.buildSearchUrl(searchTerm) + return builder.buildSearchUrl(searchTerm, securityLevel) } /** diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/internal/SearchUrlBuilder.kt @@ -29,9 +29,12 @@ private const val OS_PARAM_OPTIONAL = "\\{" + "(?:\\w+:)?\\w+?" + "\\}" internal class SearchUrlBuilder( private val searchEngine: SearchEngine, ) { - fun buildSearchUrl(searchTerms: String): String { + fun buildSearchUrl(searchTerms: String, securityLevel: Int): String { // The parser should have put the best URL for this device at the beginning of the list. - val template = searchEngine.resultUrls[0] + var template = searchEngine.resultUrls[0] + if (securityLevel == 1 && (searchEngine.id == "ddg" || searchEngine.id == "ddg-onion")) { + template = template.replaceFirst("/?", "/html/?") + } return buildUrl(template, searchTerms) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/UseCases.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/UseCases.kt @@ -80,6 +80,7 @@ class UseCases( store.value, tabsUseCases, sessionUseCases, + engine.value.settings, ) } diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt @@ -1057,6 +1057,7 @@ package org.mozilla.geckoview { method @NonNull public String getSameDocumentNavigationOverridesLoadTypeForceDisable(); method @Nullable public Rect getScreenSizeOverride(); method public boolean getSpoofEnglish(); + method public int getTorSecurityLevel(); method public boolean getTranslationsOfferPopup(); method @NonNull public List<String> getTrustedRecursiveResolverExcludedDomains(); method @NonNull public String getTrustedRecursiveResolverUri(); @@ -1112,6 +1113,7 @@ package org.mozilla.geckoview { method @NonNull public GeckoRuntimeSettings setSameDocumentNavigationOverridesLoadType(boolean); method @NonNull public GeckoRuntimeSettings setSameDocumentNavigationOverridesLoadTypeForceDisable(@NonNull String); method @NonNull public GeckoRuntimeSettings setSpoofEnglish(boolean); + method @NonNull public GeckoRuntimeSettings setTorSecurityLevel(int); method @NonNull public GeckoRuntimeSettings setTranslationsOfferPopup(boolean); method @NonNull public GeckoRuntimeSettings setTrustedRecursiveResolverExcludedDomains(@NonNull List<String>); method @NonNull public GeckoRuntimeSettings setTrustedRecursiveResolverMode(int); @@ -1184,6 +1186,7 @@ package org.mozilla.geckoview { method @NonNull public GeckoRuntimeSettings.Builder setSameDocumentNavigationOverridesLoadType(boolean); method @NonNull public GeckoRuntimeSettings.Builder setSameDocumentNavigationOverridesLoadTypeForceDisable(@NonNull String); method @NonNull public GeckoRuntimeSettings.Builder spoofEnglish(boolean); + method @NonNull public GeckoRuntimeSettings.Builder torSecurityLevel(int); method @NonNull public GeckoRuntimeSettings.Builder translationsOfferPopup(boolean); method @NonNull public GeckoRuntimeSettings.Builder trustedRecursiveResolverMode(int); method @NonNull public GeckoRuntimeSettings.Builder trustedRecursiveResolverUri(@NonNull String); diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java @@ -749,6 +749,17 @@ public final class GeckoRuntimeSettings extends RuntimeSettings { getSettings().mSpoofEnglish.set(flag ? 2 : 1); return this; } + + /** + * Set security level. + * + * @param level A value determining the security level. Default is 0. + * @return This Builder instance. + */ + public @NonNull Builder torSecurityLevel(final int level) { + getSettings().mSecurityLevel.set(level); + return this; + } } private GeckoRuntime mRuntime; @@ -881,6 +892,8 @@ public final class GeckoRuntimeSettings extends RuntimeSettings { /* package */ final PrefWithoutDefault<String> mCrliteChannel = new PrefWithoutDefault<String>("security.pki.crlite_channel"); /* package */ final Pref<Integer> mSpoofEnglish = new Pref<>("privacy.spoof_english", 0); + /* package */ final Pref<Integer> mSecurityLevel = + new Pref<>("browser.security_level.security_slider", 4); /* package */ int mPreferredColorScheme = COLOR_SCHEME_SYSTEM; @@ -2552,6 +2565,26 @@ public final class GeckoRuntimeSettings extends RuntimeSettings { return this; } + /** + * Gets the current security level. + * + * @return current security protection level + */ + public int getTorSecurityLevel() { + return mSecurityLevel.get(); + } + + /** + * Sets the Security Level. + * + * @param level security protection level + * @return This GeckoRuntimeSettings instance. + */ + public @NonNull GeckoRuntimeSettings setTorSecurityLevel(final int level) { + mSecurityLevel.commit(level); + return this; + } + @Override // Parcelable public void writeToParcel(final Parcel out, final int flags) { super.writeToParcel(out, flags); diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in @@ -123,6 +123,9 @@ @BINPATH@/chrome/devtools@JAREXT@ @BINPATH@/chrome/devtools.manifest +; Base Browser +@BINPATH@/components/SecurityLevel.manifest + ; [Default Preferences] ; All the pref files must be part of base to prevent migration bugs #ifndef MOZ_ANDROID_FAT_AAR_ARCHITECTURES diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build @@ -75,6 +75,7 @@ DIRS += [ "reportbrokensite", "resistfingerprinting", "search", + "securitylevel", "sessionstore", "shell", "startup", diff --git a/toolkit/components/search/SearchEngineSelector.sys.mjs b/toolkit/components/search/SearchEngineSelector.sys.mjs @@ -201,6 +201,9 @@ export class SearchEngineSelector { * The name of the application. * @param {string} [options.version] * The version of the application. + * @param {boolean} [options.javascriptEnabled] + * Tell whether JS is enabled. If not, we will prefer plain HTML version of + * search engines, when available. * @returns {Promise<RefinedSearchConfig>} * An object which contains the refined configuration with a filtered list * of search engines, and the identifiers for the application default engines. @@ -213,6 +216,7 @@ export class SearchEngineSelector { experiment, appName = Services.appinfo.name ?? "", version = Services.appinfo.version ?? "", + javascriptEnabled = true, }) { if (!this.#configuration) { await this.getEngineConfiguration(); @@ -239,6 +243,17 @@ export class SearchEngineSelector { e => !e.optional ); + if (!javascriptEnabled) { + refinedSearchConfig.engines = refinedSearchConfig.engines.map(e => { + if (e.identifier === "ddg") { + e.urls.search.base = "https://html.duckduckgo.com/html"; + } else if (e.identifier === "ddg-onion") { + e.urls.search.base += "html"; + } + return e; + }); + } + if ( !refinedSearchConfig.appDefaultEngineId || !refinedSearchConfig.engines.find( diff --git a/toolkit/components/search/SearchService.sys.mjs b/toolkit/components/search/SearchService.sys.mjs @@ -25,6 +25,7 @@ const lazy = XPCOMUtils.declareLazy({ "moz-src:///toolkit/components/search/PolicySearchEngine.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SecurityLevelPrefs: "resource://gre/modules/SecurityLevel.sys.mjs", SearchEngine: "moz-src:///toolkit/components/search/SearchEngine.sys.mjs", SearchEngineSelector: "moz-src:///toolkit/components/search/SearchEngineSelector.sys.mjs", @@ -69,6 +70,7 @@ const lazy = XPCOMUtils.declareLazy({ const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed"; const QUIT_APPLICATION_TOPIC = "quit-application"; +const TOPIC_JSENABLED_CHANGED = "SecurityLevel:JavascriptEnabledChanged"; // The update timer for OpenSearch engines checks in once a day. const OPENSEARCH_UPDATE_TIMER_TOPIC = "search-engine-update-timer"; @@ -2896,6 +2898,7 @@ export class SearchService { channel: lazy.SearchUtils.MODIFIED_APP_CHANNEL, experiment: this.#lazyPrefs.experimentPrefValue, distroID: lazy.SearchUtils.distroID ?? "", + javascriptEnabled: lazy.SecurityLevelPrefs.javascriptEnabled, }; for (let [key, value] of Object.entries(searchEngineSelectorProperties)) { @@ -3802,6 +3805,7 @@ export class SearchService { Services.obs.addObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED); Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC); Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE); + Services.obs.addObserver(this, TOPIC_JSENABLED_CHANGED); this._settings.addObservers(); @@ -3864,6 +3868,7 @@ export class SearchService { Services.obs.removeObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED); Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC); Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE); + Services.obs.removeObserver(this, TOPIC_JSENABLED_CHANGED); this.#observersAdded = false; this.#earlyObserversAdded = false; } @@ -3945,6 +3950,13 @@ export class SearchService { Ci.nsISearchService.CHANGE_REASON_REGION ).catch(console.error); break; + + case TOPIC_JSENABLED_CHANGED: + lazy.logConsole.debug("JavaScript toggled"); + this._maybeReloadEngines( + Ci.nsISearchService.CHANGE_REASON_CONFIG + ).catch(console.error); + break; } } diff --git a/toolkit/components/search/tests/xpcshell/test_security_level.js b/toolkit/components/search/tests/xpcshell/test_security_level.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests that we use the HTML version of DuckDuckGo when in the safest + * security level. + */ + +"use strict"; + +const expectedURLs = { + ddg: "https://html.duckduckgo.com/html?q=test", +}; + +add_task(async function test_securityLevel() { + await Services.search.init(); + for (const [id, url] of Object.entries(expectedURLs)) { + const engine = Services.search.getEngineById(id); + const foundUrl = engine.getSubmission("test").uri.spec; + Assert.equal(foundUrl, url, `${engine.name} is in HTML mode.`); + } +}); diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.toml b/toolkit/components/search/tests/xpcshell/xpcshell.toml @@ -200,6 +200,9 @@ support-files = [ ["test_searchUrlDomain.js"] +["test_security_level.js"] +prefs = ["browser.security_level.security_slider=1"] + ["test_selectedEngine.js"] ["test_sendSubmissionURL.js"] diff --git a/toolkit/components/securitylevel/SecurityLevel.manifest b/toolkit/components/securitylevel/SecurityLevel.manifest @@ -0,0 +1 @@ +category profile-after-change SecurityLevel @torproject.org/security-level;1 diff --git a/toolkit/components/securitylevel/SecurityLevel.sys.mjs b/toolkit/components/securitylevel/SecurityLevel.sys.mjs @@ -0,0 +1,831 @@ +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + +const logger = console.createInstance({ + maxLogLevel: "Info", + prefix: "SecurityLevel", +}); + +const BrowserTopics = Object.freeze({ + ProfileAfterChange: "profile-after-change", +}); + +// 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. +var getPrefValue = function (prefName) { + switch (Services.prefs.getPrefType(prefName)) { + case Services.prefs.PREF_BOOL: + return Services.prefs.getBoolPref(prefName); + case Services.prefs.PREF_INT: + return Services.prefs.getIntPref(prefName); + case Services.prefs.PREF_STRING: + return Services.prefs.getCharPref(prefName); + default: + return null; + } +}; + +// __bindPref(prefName, prefHandler)__ +// Applies prefHandler whenever the value of the pref changes. +// If init is true, applies prefHandler to the current value. +// Returns the observer that was added. +var bindPref = function (prefName, prefHandler) { + let update = () => { + prefHandler(getPrefValue(prefName)); + }, + observer = { + observe(subject, topic, data) { + if (data === prefName) { + update(); + } + }, + }; + Services.prefs.addObserver(prefName, observer); + return observer; +}; + +async function waitForExtensionMessage(extensionId, checker = () => {}) { + const { torWaitForExtensionMessage } = lazy.ExtensionParent; + if (torWaitForExtensionMessage) { + return torWaitForExtensionMessage(extensionId, checker); + } + return undefined; +} + +async function sendExtensionMessage(extensionId, message) { + const { torSendExtensionMessage } = lazy.ExtensionParent; + if (torSendExtensionMessage) { + return await torSendExtensionMessage(extensionId, message); + } + return undefined; +} + +// ## NoScript settings + +// Minimum and maximum capability states as controlled by NoScript. +const max_caps = [ + "fetch", + "font", + "frame", + "media", + "object", + "other", + "script", + "wasm", + "webgl", + "noscript", +]; +const min_caps = ["frame", "other", "noscript"]; + +// Untrusted capabilities for [Standard, Safer, Safest] safety levels. +const untrusted_caps = [ + max_caps, // standard safety: neither http nor https + ["frame", "font", "object", "other", "noscript"], // safer: http + min_caps, // safest: neither http nor https +]; + +// Default capabilities for [Standard, Safer, Safest] safety levels. +const default_caps = [ + max_caps, // standard: both http and https + ["fetch", "font", "frame", "object", "other", "script", "noscript"], // safer: https only + min_caps, // safest: both http and https +]; + +// __noscriptSettings(safetyLevel)__. +// Produces NoScript settings with policy according to +// the safetyLevel which can be: +// 0 = Standard, 1 = Safer, 2 = Safest +// +// At the "Standard" safety level, we leave all sites at +// default with maximal capabilities. Essentially no content +// is blocked. +// +// At "Safer", we set all http sites to untrusted, +// and all https sites to default. Scripts are only permitted +// on https sites. Neither type of site is supposed to allow +// media, but both allow fonts (as we used in legacy NoScript). +// +// At "Safest", all sites are at default with minimal +// capabilities. Most things are blocked. +let noscriptSettings = safetyLevel => ({ + __meta: { + name: "updateSettings", + recipientInfo: null, + }, + policy: { + DEFAULT: { + capabilities: default_caps[safetyLevel], + temp: false, + }, + TRUSTED: { + capabilities: max_caps, + temp: false, + }, + UNTRUSTED: { + capabilities: untrusted_caps[safetyLevel], + temp: false, + }, + sites: { + trusted: [], + untrusted: [[], ["http:"], []][safetyLevel], + custom: {}, + temp: [], + }, + enforced: true, + autoAllowTop: false, + }, + isTorBrowser: true, + tabId: -1, +}); + +// ## Communications + +// The extension ID for NoScript (WebExtension) +const noscriptID = "{73a6fe31-595d-460b-a920-fcc0f8843232}"; + +// Ensure binding only occurs once. +let initialized = false; + +// __initialize()__. +// The main function that binds the NoScript settings to the security +// slider pref state. +var initializeNoScriptControl = () => { + if (initialized) { + return; + } + initialized = true; + + try { + // LegacyExtensionContext is not there anymore. Using raw + // Services.cpmm.sendAsyncMessage mechanism to communicate with + // NoScript. + + // The component that handles WebExtensions' sendMessage. + + // __setNoScriptSettings(settings)__. + // NoScript listens for internal settings with onMessage. We can send + // a new settings JSON object according to NoScript's + // protocol and these are accepted! See the use of + // `browser.runtime.onMessage.addListener(...)` in NoScript's bg/main.js. + + // TODO: Is there a better way? + let sendNoScriptSettings = async settings => + await sendExtensionMessage(noscriptID, settings); + + // __securitySliderToSafetyLevel(sliderState)__. + // Converts the "browser.security_level.security_slider" pref value + // to a "safety level" value: 0 = Standard, 1 = Safer, 2 = Safest + let securitySliderToSafetyLevel = sliderState => + [undefined, 2, 1, 1, 0][sliderState]; + + // Wait for the first message from NoScript to arrive, and then + // bind the security_slider pref to the NoScript settings. + let messageListener = async a => { + try { + logger.debug("Message received from NoScript:", a); + const persistPref = "browser.security_level.noscript_persist"; + let noscriptPersist = Services.prefs.getBoolPref(persistPref, false); + let noscriptInited = Services.prefs.getBoolPref( + kNoScriptInitedPref, + false + ); + // 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.error("Could not apply NoScript settings", e); + // Treat as a custom security level for the rest of the session. + SecurityLevelPrefs.setCustomAndWarn(); + } + }; + waitForExtensionMessage(noscriptID, a => a.__meta.name === "started").then( + messageListener + ); + logger.info("Listening for messages from NoScript."); + } catch (e) { + logger.exception(e); + // Treat as a custom security level for the rest of the session. + SecurityLevelPrefs.setCustomAndWarn(); + } +}; + +// ### Constants + +// __kSecuritySettings__. +// A table of all prefs bound to the security slider, and the value +// for each security setting. Note that 2-m and 3-m are identical, +// corresponding to the old 2-medium-high setting. We also separately +// bind NoScript settings to the browser.security_level.security_slider +/* eslint-disable */ +// prettier-ignore +const kSecuritySettings = { + // Preference name: [0, 1-high 2-m 3-m 4-low] + "javascript.options.ion": [, false, false, false, true ], + "javascript.options.baselinejit": [, false, false, false, true ], + "javascript.options.native_regexp": [, false, false, false, true ], + "mathml.disabled": [, true, true, true, false], + "gfx.font_rendering.graphite.enabled": [, false, false, false, true ], + "gfx.font_rendering.opentype_svg.enabled": [, false, false, false, true ], + "svg.disabled": [, true, false, false, false], + "javascript.options.asmjs": [, false, false, false, true ], + // tor-browser#44234, tor-browser#44242: this interferes with the correct + // functioning of the browser. So, WASM is also handled by NoScript now. + "javascript.options.wasm": [, true, true, true, true ], +}; +/* eslint-enable */ + +// ### 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) { + 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()__. +// Read the current pref values, and decide if any of our +// security settings matches. Otherwise return null. +var read_setting_from_prefs = function (prefNames) { + prefNames = prefNames || Object.keys(kSecuritySettings); + for (const settingIndex of [1, 2, 3, 4]) { + let possibleSetting = true; + // For the given settingIndex, check if all current pref values + // match the setting. + for (const prefName of prefNames) { + const wanted = kSecuritySettings[prefName][settingIndex]; + const actual = Services.prefs.getBoolPref(prefName); + if (wanted !== actual) { + possibleSetting = false; + logger.debug( + `${prefName} does not match level ${settingIndex}: ${actual}, should be ${wanted}!` + ); + break; + } + } + if (possibleSetting) { + logger.debug(`Preferences match level ${settingIndex}.`); + return settingIndex; + } + } + // No matching setting; return null. + return null; +}; + +// __initialized__. +// Have we called initialize() yet? +var initializedSecPrefs = false; + +// __initialize()__. +// Defines the behavior of "browser.security_level.security_custom", +// "browser.security_level.security_slider", and the security-sensitive +// prefs declared in kSecuritySettings. +var initializeSecurityPrefs = function () { + // Only run once. + if (initializedSecPrefs) { + return; + } + logger.info("Initializing security level"); + initializedSecPrefs = true; + + 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); + + if (!(wasCustom && desiredIndex == 4)) { + // The current level is non-customized Standard, or + // Safer / Safest (either customized or not): the global + // javascript.options.wasm pref interferes with the correct + // functioning of the browser, so instead we rely on NoScript + // to disable WebAssembly now (tor-browser#44234, tor-browser#44242). + // We skip flipping in customized Standard, because if its value was + // found false under such as circumstance, that would suggest + // an intentional user choice we don't want to interfere with. + // Unlike other javascript.options.* preferences, this one is safe + // to flip without a browser restart because it's checked whenever a + // context is created. + Services.prefs.setBoolPref("javascript.options.wasm", true); + } + // 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); + Services.prefs.setIntPref(kSliderPref, effectiveIndex); + } + // Determine the javascriptEnabled value *after* we have set kSliderPref. + SecurityLevelPrefs.updateJavascriptEnabled(); + // Warn the user if they have booted the browser in a custom state, and have + // not yet acknowledged it in a previous session. + SecurityLevelPrefs.maybeWarnCustom(); + + 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(); + } + }), + }); + 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(); + }), + }); + + 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. + SecurityLevelPrefs.setCustomAndWarn(); + }), + }); + } + + logger.info("Security level initialization complete"); +}; + +// tor-browser#41460: we changed preference names in 12.0. +// 11.5.8 is an obligated step for desktop users, so this code is helpful only +// to alpha users, and we could remove it quite soon. +function migratePreferences() { + const kPrefCheck = "extensions.torbutton.noscript_inited"; + // For 12.0, check for extensions.torbutton.noscript_inited, which was set + // as a user preference for sure, if someone used security level in previous + // versions. + if (!Services.prefs.prefHasUserValue(kPrefCheck)) { + return; + } + const migrate = (oldName, newName, getter, setter) => { + oldName = `extensions.torbutton.${oldName}`; + newName = `browser.${newName}`; + if (Services.prefs.prefHasUserValue(oldName)) { + setter(newName, getter(oldName)); + Services.prefs.clearUserPref(oldName); + } + }; + const prefs = { + security_custom: "security_level.security_custom", + noscript_persist: "security_level.noscript_persist", + noscript_inited: "security_level.noscript_inited", + }; + for (const [oldName, newName] of Object.entries(prefs)) { + migrate( + oldName, + newName, + Services.prefs.getBoolPref.bind(Services.prefs), + Services.prefs.setBoolPref.bind(Services.prefs) + ); + } + migrate( + "security_slider", + "security_level.security_slider", + Services.prefs.getIntPref.bind(Services.prefs), + Services.prefs.setIntPref.bind(Services.prefs) + ); +} + +/** + * This class is used to initialize the security level stuff at the startup + */ +export class SecurityLevel { + QueryInterface = ChromeUtils.generateQI(["nsIObserver"]); + + init() { + migratePreferences(); + // Fixup our preferences before we pass on the security level to NoScript. + initializeSecurityPrefs(); + initializeNoScriptControl(); + } + + observe(aSubject, aTopic) { + if (aTopic === BrowserTopics.ProfileAfterChange) { + this.init(); + } + } +} + +/** + * @callback SecurityLevelTryRestartBrowserCallback + * + * @returns {Promise<boolean>} - A promise that resolves when the user has made + * a decision. Should return `true` when the browser is now restarting. + */ +/** + * @callback SecurityLevelShowCustomWarningCallback + * + * @param {Function} userDismissedCallback - A callback that should be called + * if the user has acknowledged and dismissed the notification. + */ +/** + * @typedef {object} SecurityLevelNotificationHandler + * + * An object that can serve the user notifications. + * + * @property {SecurityLevelTryRestartBrowserCallback} tryRestartBrowser - The + * method that should be called to ask the user to restart the browser. + * @property {SecurityLevelShowCustomWarningCallback} showCustomWarning - The + * method that should be called to let the user know they have a custom + * security level. + */ + +/* + Security Level Prefs + + Getters and Setters for relevant security level prefs +*/ +export const SecurityLevelPrefs = { + SecurityLevels: Object.freeze({ + safest: 1, + safer: 2, + standard: 4, + }), + security_slider_pref: "browser.security_level.security_slider", + security_custom_pref: "browser.security_level.security_custom", + _customWarningDismissedPref: + "browser.security_level.custom_warning_dismissed", + + /** + * 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. + const val = Services.prefs.getIntPref(this.security_slider_pref, 0); + return Object.entries(this.SecurityLevels).find( + entry => entry[1] === val + )?.[0]; + }, + + /** + * Cached value for whether javascript is enabled. `null` whilst undetermined. + * + * @type {?boolean} + */ + _javascriptEnabled: null, + + /** + * Whether javascript is enabled for web pages at the current security level. + * + * @type {boolean} + */ + get javascriptEnabled() { + if (this._javascriptEnabled === null) { + this.updateJavascriptEnabled(); + } + return this._javascriptEnabled; + }, + + /** + * Update the javascriptEnabled value. + */ + updateJavascriptEnabled() { + // NoScript will disable javascript for web pages at the safest security + // level. + const enabled = this.securityLevel !== "safest"; + if (enabled === this._javascriptEnabled) { + return; + } + this._javascriptEnabled = enabled; + Services.obs.notifyObservers( + null, + "SecurityLevel:JavascriptEnabledChanged" + ); + }, + + /** + * 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]); + // NOTE: Do not call `updateJavascriptEnabled`. We are about to restart, so + // consumers do not need to know about the change. + // Moreover, the change has not reached NoScript, which controls the + // javascript changes. + }, + + /** + * 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); + }, + + /** + * 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"; + }, + + /** + * The external handler that can show a notification to the user, if any. + * + * @type {?SecurityLevelNotificationHandler} + */ + _notificationHandler: null, + + /** + * The notifications we are waiting for a handler to show. + * + * @type {Set} + */ + _pendingNotifications: {}, + + /** + * 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 {SecurityLevelNotificationHandler} handler - The new handler to use. + */ + setNotificationHandler(handler) { + logger.info("Restart notification handler is set"); + this._notificationHandler = handler; + this._tryShowNotifications(this._pendingNotifications); + }, + + /** + * A promise for any ongoing notification prompt task. + * + * Resolves with whether the browser is restarting. + * + * @type {Promise<boolean>} + */ + _restartNotificationPromise: null, + + /** + * Try show notifications to the user. + * + * If no notification handler has been attached yet, this will queue the + * notification for when it is added, if ever. + * + * @param {object} notifications - The notifications to try and show. + * @param {boolean} notifications.restart - Whether to show the restart + * notification. + * @param {boolean} notifications.custom - Whether to show the custom security + * level notification. + */ + async _tryShowNotifications(notifications) { + if (!this._notificationHandler) { + logger.info("Missing a notification handler", notifications); + // This may be added later in the session. + if (notifications.custom) { + this._pendingNotifications.custom = true; + } + if (notifications.restart) { + this._pendingNotifications.restart = true; + } + return; + } + + let isRestarting = false; + if (notifications.restart) { + const prevPromise = this._restartNotificationPromise; + let resolve; + ({ promise: this._restartNotificationPromise, resolve } = + Promise.withResolvers()); + await prevPromise; + + try { + isRestarting = await this._notificationHandler?.tryRestartBrowser(); + } finally { + // Allow the notification to be shown again. + resolve(); + } + } + // NOTE: We wait for the restart notification to resolve before showing the + // custom warning. We do not show the warning if we are already restarting. + if (!isRestarting && notifications.custom) { + this._notificationHandler?.showCustomWarning(() => { + // User has acknowledged and dismissed the notification. + Services.prefs.setBoolPref(this._customWarningDismissedPref, true); + }); + } + + this._pendingNotifications = {}; + }, + + /** + * 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); + + // 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. + // NOTE: Do not call `updateJavascriptEnabled` because the change has not + // reached NoScript, which controls the javascript changes. + + this._tryShowNotifications({ restart: true, custom: true }); + }, + + /** + * Put the user in the custom security level state and show them a warning + * about this state. + */ + setCustomAndWarn() { + Services.prefs.setBoolPref(kCustomPref, true); + // NOTE: We clear _customWarningDismissedPref because the entry points + // for this method imply we should re-warn the user each time. + Services.prefs.clearUserPref(this._customWarningDismissedPref); + this._tryShowNotifications({ custom: true }); + }, + + /** + * If the user is in a custom state, try and notify them of this state. + */ + maybeWarnCustom() { + const isCustom = Services.prefs.getBoolPref(kCustomPref, false); + if (!isCustom) { + // Clear the dismissed preference so the user will be re-shown the + // notification when they re-enter the custom state. + Services.prefs.clearUserPref(this._customWarningDismissedPref); + return; + } + if (Services.prefs.getBoolPref(this._customWarningDismissedPref, false)) { + // Do not warn the user of the custom state if they have already + // acknowledged and dismissed this in a previous session. + return; + } + + this._tryShowNotifications({ custom: true }); + }, +}; /* Security Level Prefs */ diff --git a/toolkit/components/securitylevel/components.conf b/toolkit/components/securitylevel/components.conf @@ -0,0 +1,10 @@ +Classes = [ + { + "cid": "{c602ffe5-abf4-40d0-a944-26738b81efdb}", + "contract_ids": [ + "@torproject.org/security-level;1", + ], + "esModule": "resource://gre/modules/SecurityLevel.sys.mjs", + "constructor": "SecurityLevel", + } +] diff --git a/toolkit/components/securitylevel/moz.build b/toolkit/components/securitylevel/moz.build @@ -0,0 +1,11 @@ +EXTRA_JS_MODULES += [ + "SecurityLevel.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXTRA_COMPONENTS += [ + "SecurityLevel.manifest", +]