commit 6aa548f73a7d91671d3f272a7463b1de27117755 parent a0901b99cc1f5876f6e81b899a42559b55eeb1c0 Author: Richard Pospesel <richard@torproject.org> Date: Wed, 28 Apr 2021 23:09:34 -0500 TB 27476: Implement about:torconnect captive portal within Tor Browser - implements new about:torconnect page as tor-launcher replacement - adds new torconnect component to browser - tor process management functionality remains implemented in tor-launcher through the TorProtocolService module - adds warning/error box to about:preferences#tor when not connected to tor Bug 40773: Update the about:torconnect frontend page to match additional UI flows. Bug 41608: Add a toolbar status button and a urlbar "Connect" button. Diffstat:
40 files changed, 2415 insertions(+), 37 deletions(-)
diff --git a/browser/.eslintrc.mjs b/browser/.eslintrc.mjs @@ -7,7 +7,7 @@ export default [ rules: { // XXX Bug 1326071 - This should be reduced down - probably to 20 or to // be removed & synced with the mozilla/recommended value. - complexity: ["error", { max: 44 }], + complexity: ["error", { max: 48 }], // Disallow empty statements. This will report an error for: // try { something(); } catch (e) {} diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js @@ -246,6 +246,9 @@ var gBrowserInit = { // Init the SecurityLevelButton SecurityLevelButton.init(); + gTorConnectUrlbarButton.init(); + gTorConnectTitlebarStatus.init(); + gTorCircuitPanel.init(); // Certain kinds of automigration rely on this notification to complete @@ -1011,32 +1014,43 @@ var gBrowserInit = { let defaultArgs = BrowserHandler.defaultArgs; - // If the given URI is different from the homepage, we want to load it. - if (uri != defaultArgs) { - AboutNewTab.noteNonDefaultStartup(); + // figure out which URI to actually load (or a Promise to get the uri) + uri = (aUri => { + // If the given URI is different from the homepage, we want to load it. + if (aUri != defaultArgs) { + AboutNewTab.noteNonDefaultStartup(); + + if (aUri instanceof Ci.nsIArray) { + // Transform the nsIArray of nsISupportsString's into a JS Array of + // JS strings. + return Array.from( + aUri.enumerate(Ci.nsISupportsString), + supportStr => supportStr.data + ); + } else if (aUri instanceof Ci.nsISupportsString) { + return aUri.data; + } + return aUri; + } - if (uri instanceof Ci.nsIArray) { - // Transform the nsIArray of nsISupportsString's into a JS Array of - // JS strings. - return Array.from( - uri.enumerate(Ci.nsISupportsString), - supportStr => supportStr.data - ); - } else if (uri instanceof Ci.nsISupportsString) { - return uri.data; + // The URI appears to be the the homepage. We want to load it only if + // session restore isn't about to override the homepage. + let willOverride = SessionStartup.willOverrideHomepage; + if (typeof willOverride == "boolean") { + return willOverride ? null : uri; } - return uri; - } + return willOverride.then(willOverrideHomepage => + willOverrideHomepage ? null : uri + ); + })(uri); - // The URI appears to be the the homepage. We want to load it only if - // session restore isn't about to override the homepage. - let willOverride = SessionStartup.willOverrideHomepage; - if (typeof willOverride == "boolean") { - return willOverride ? null : uri; + // if using TorConnect, convert these uris to redirects + if (TorConnect.shouldShowTorConnect) { + return Promise.resolve(uri).then(aUri => + TorConnectParent.getURIsToLoad(aUri ?? []) + ); } - return willOverride.then(willOverrideHomepage => - willOverrideHomepage ? null : uri - ); + return uri; })()); }, @@ -1100,6 +1114,9 @@ var gBrowserInit = { SecurityLevelButton.uninit(); + gTorConnectUrlbarButton.uninit(); + gTorConnectTitlebarStatus.uninit(); + gTorCircuitPanel.uninit(); if (gToolbarKeyNavEnabled) { diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -103,6 +103,10 @@ ChromeUtils.defineESModuleGetters(this, { ToolbarDropHandler: "moz-src:///browser/components/customizableui/ToolbarDropHandler.sys.mjs", ToolbarIconColor: "moz-src:///browser/themes/ToolbarIconColor.sys.mjs", + TorConnect: "resource://gre/modules/TorConnect.sys.mjs", + TorConnectStage: "resource://gre/modules/TorConnect.sys.mjs", + TorConnectTopics: "resource://gre/modules/TorConnect.sys.mjs", + TorConnectParent: "resource://gre/actors/TorConnectParent.sys.mjs", TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", TorUIUtils: "resource:///modules/TorUIUtils.sys.mjs", TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", @@ -315,6 +319,16 @@ XPCOMUtils.defineLazyScriptGetter( ); XPCOMUtils.defineLazyScriptGetter( this, + ["gTorConnectUrlbarButton"], + "chrome://global/content/torconnect/torConnectUrlbarButton.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, + ["gTorConnectTitlebarStatus"], + "chrome://global/content/torconnect/torConnectTitlebarStatus.js" +); +XPCOMUtils.defineLazyScriptGetter( + this, ["gTorCircuitPanel"], "chrome://browser/content/torCircuitPanel.js" ); @@ -738,6 +752,7 @@ var gPageIcons = { }; var gInitialPages = [ + "about:torconnect", "about:blank", "about:home", "about:firefoxview", diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals @@ -243,5 +243,11 @@ "NewIdentityButton", "TorUIUtils", "TorDomainIsolator", - "gTorCircuitPanel" + "gTorCircuitPanel", + "TorConnect", + "TorConnectStage", + "TorConnectTopics", + "TorConnectParent", + "gTorConnectUrlbarButton", + "gTorConnectTitlebarStatus" ] diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml @@ -58,6 +58,7 @@ <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelPanel.css" /> <link rel="stylesheet" href="chrome://browser/content/securitylevel/securityLevelButton.css" /> <link rel="stylesheet" href="chrome://browser/content/torCircuitPanel.css" /> + <link rel="stylesheet" href="chrome://global/content/torconnect/torConnectTitlebarStatus.css" /> <link rel="localization" href="branding/brand.ftl"/> <link rel="localization" href="browser/allTabsMenu.ftl"/> diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml @@ -114,6 +114,7 @@ <toolbarbutton class="content-analysis-indicator toolbarbutton-1 content-analysis-indicator-icon"/> +#include ../../../toolkit/components/torconnect/content/torConnectTitlebarStatus.inc.xhtml #include titlebar-items.inc.xhtml </toolbar> @@ -431,6 +432,13 @@ <image id="star-button" class="urlbar-icon"/> </hbox> + + <hbox id="tor-connect-urlbar-button" + role="button" + class="tor-button tor-urlbar-button" + hidden="true"> + <label id="tor-connect-urlbar-button-label"/> + </hbox> </hbox> </html:moz-urlbar> <toolbartabstop/> @@ -516,6 +524,7 @@ <hbox class="titlebar-spacer" type="post-tabs"/> #include private-browsing-indicator.inc.xhtml <toolbarbutton class="content-analysis-indicator toolbarbutton-1 content-analysis-indicator-icon"/> +#include ../../../toolkit/components/torconnect/content/torConnectTitlebarStatus.inc.xhtml #include titlebar-items.inc.xhtml </toolbar> diff --git a/browser/modules/URILoadingHelper.sys.mjs b/browser/modules/URILoadingHelper.sys.mjs @@ -12,6 +12,8 @@ ChromeUtils.defineESModuleGetters(lazy, { AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", + TorConnect: "resource://gre/modules/TorConnect.sys.mjs", + TorConnectParent: "resource://gre/actors/TorConnectParent.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () => @@ -460,6 +462,28 @@ export const URILoadingHelper = { return; } + // make sure users are not faced with the scary red 'tor isn't working' screen + // if they navigate to about:tor before bootstrapped + // + // fixes tor-browser#40752 + // new tabs also redirect to about:tor if browser.newtabpage.enabled is true + // otherwise they go to about:blank + if (lazy.TorConnect.shouldShowTorConnect) { + const homeURLs = [ + "about:home", + "about:privatebrowsing", + "about:tor", + "about:welcome", + ]; + if ( + homeURLs.includes(url) || + (url === "about:newtab" && + Services.prefs.getBoolPref("browser.newtabpage.enabled", false)) + ) { + url = lazy.TorConnectParent.getRedirectURL(url); + } + } + let { allowThirdPartyFixup, postData, diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css @@ -30,6 +30,7 @@ @import url("chrome://browser/skin/UITour.css"); @import url("chrome://browser/skin/formautofill-notification.css"); @import url("chrome://global/skin/tor-colors.css"); +@import url("chrome://browser/skin/tor-urlbar-button.css"); :root, body { diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn @@ -47,6 +47,7 @@ skin/classic/browser/urlbar/sports-american-football.svg (../shared/urlbar/sports-american-football.svg) skin/classic/browser/urlbar/sports-basketball.svg (../shared/urlbar/sports-basketball.svg) skin/classic/browser/urlbar/sports-hockey.svg (../shared/urlbar/sports-hockey.svg) + skin/classic/browser/tor-urlbar-button.css (../shared/tor-urlbar-button.css) skin/classic/browser/urlbar-dynamic-results.css (../shared/urlbar-dynamic-results.css) skin/classic/browser/urlbar-searchbar.css (../shared/urlbar-searchbar.css) skin/classic/browser/urlbarView.css (../shared/urlbarView.css) diff --git a/browser/themes/shared/tor-urlbar-button.css b/browser/themes/shared/tor-urlbar-button.css @@ -0,0 +1,73 @@ +.tor-urlbar-button:not([hidden]) { + display: flex; + align-items: center; + gap: 0.5em; + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-border-radius-tokens */ + border-radius: var(--urlbar-inner-border-radius); + --tor-urlbar-button-inline-padding: 8px; + padding-inline: var(--tor-urlbar-button-inline-padding); + margin: 0; +} + +.tor-urlbar-button > * { + flex: 0 0 auto; + margin: 0; +} + +.tor-urlbar-button:focus-visible { + /* This button lies within the urlbar, so if the outline extends beyond the + * button's boundary, it will be clipped by the urlbar. + * Most button's in the urlbar get around this by using --focus-outline-inset, + * but our button has a purple background, which does not contrast well with + * the focus outline. + * Therefore, we use an offset outline rather than an inset outline, and + * compensate by shrinking the button's width and height so that the outline + * fits within the non-focused button boundary. Essentially, this has a + * similar effect to using an inset outline that matches the color of the + * urlbar background, but we keep the rounded corners. */ + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); + /* Calculate the difference between the button's border area and the outline + * area. */ + --tor-urlbar-focus-outline-difference: calc(var(--focus-outline-offset) + var(--focus-outline-width)); + /* For the inline direction, we shrink the padding by the difference, and + * increase the margin by the same amount so that the button text remains in + * the same position. + * For the block direction, the height of the button is flexibly sized with + * the urlbar height, so we should only need to increase the margin. */ + padding-inline: calc(var(--tor-urlbar-button-inline-padding) - var(--tor-urlbar-focus-outline-difference)); + margin: var(--tor-urlbar-focus-outline-difference); +} + +.tor-urlbar-button:focus-visible > * { + /* Negate the margin that would be added on focus to ensure the button does + * not grow in height. + * Ideally, this should not change anything noticeable, otherwise the text + * could be clipped when focused. */ + margin-block: calc(-1 * var(--tor-urlbar-focus-outline-difference)); +} + +#urlbar[usertyping] .tor-urlbar-button { + /* Hide whilst the user is typing in the url bar. */ + display: none; +} + +/* Make the button look plain like the identity #urlbar-label-box. */ +.tor-urlbar-button.tor-urlbar-button-plain { + background-color: var(--urlbar-box-bgcolor); + color: var(--urlbar-box-text-color); +} + +.tor-urlbar-button.tor-urlbar-button-plain:focus-visible { + outline-color: var(--focus-outline-color); +} + +.tor-urlbar-button.tor-urlbar-button-plain:hover { + background-color: var(--urlbar-box-hover-bgcolor); + color: var(--urlbar-box-hover-text-color); +} + +.tor-urlbar-button.tor-urlbar-button-plain:hover:active { + background-color: var(--urlbar-box-active-bgcolor); + color: var(--urlbar-box-hover-text-color); +} diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp @@ -221,6 +221,11 @@ static const RedirEntry kRedirMap[] = { {"telemetry", "chrome://global/content/aboutTelemetry.xhtml", nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, #endif + {"torconnect", "chrome://global/content/torconnect/aboutTorConnect.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::HIDE_FROM_ABOUTABOUT | + nsIAboutModule::IS_SECURE_CHROME_UI}, #ifndef BASE_BROWSER_VERSION // Remove about:translations since translations are disabled. // See tor-browser#44045 and tor-browser#42872. diff --git a/docshell/build/components.conf b/docshell/build/components.conf @@ -30,6 +30,7 @@ about_pages = [ 'serviceworkers', 'srcdoc', 'support', + 'torconnect', # Removed 'url-classifier'. tor-browser#42831. ] diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp @@ -6007,6 +6007,9 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) { nsDocShell::Cast(mDocShell)->GetSessionHistory(); if (!StringBeginsWith(url, u"about:neterror"_ns) && + // we want about:torconnect pages to be able to close themselves after + // bootstrap + !StringBeginsWith(url, u"about:torconnect"_ns) && !mBrowsingContext->GetTopLevelCreatedByWebContent() && !aTrustedCaller && csh && csh->Count() > 1) { bool allowClose = diff --git a/eslint-file-globals.config.mjs b/eslint-file-globals.config.mjs @@ -136,6 +136,8 @@ export default [ "toolkit/components/printing/content/printUtils.js", "browser/components/newidentity/content/newidentity.js", "browser/components/torcircuit/content/torCircuitPanel.js", + "toolkit/components/torconnect/content/torConnectTitlebarStatus.js", + "toolkit/components/torconnect/content/torConnectUrlbarButton.js", ], languageOptions: { globals: mozilla.environments["browser-window"].globals, @@ -389,6 +391,7 @@ export default [ "toolkit/content/aboutNetError.mjs", "toolkit/content/aboutNetErrorHelpers.mjs", "toolkit/content/net-error-card.mjs", + "toolkit/components/torconnect/content/aboutTorConnect.js", ], languageOptions: { globals: mozilla.environments["remote-page"].globals }, }, diff --git a/toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs b/toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs @@ -4,6 +4,8 @@ import { EscapablePageParent } from "resource://gre/actors/NetErrorParent.sys.mjs"; +import { TorConnect } from "resource://gre/modules/TorConnect.sys.mjs"; + export class AboutHttpsOnlyErrorParent extends EscapablePageParent { get browser() { return this.browsingContext.top.embedderElement; @@ -14,6 +16,9 @@ export class AboutHttpsOnlyErrorParent extends EscapablePageParent { case "goBack": this.leaveErrorPage(this.browser); break; + case "ShouldShowTorConnect": + return TorConnect.shouldShowTorConnect; } + return undefined; } } diff --git a/toolkit/actors/NetErrorParent.sys.mjs b/toolkit/actors/NetErrorParent.sys.mjs @@ -16,6 +16,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", HomePage: "resource:///modules/HomePage.sys.mjs", + TorConnect: "resource://gre/modules/TorConnect.sys.mjs", }); class CaptivePortalObserver { @@ -278,6 +279,9 @@ export class NetErrorParent extends EscapablePageParent { win.openPreferences("privacy-doh"); break; } + case "ShouldShowTorConnect": + return lazy.TorConnect.shouldShowTorConnect; } + return undefined; } } diff --git a/toolkit/components/httpsonlyerror/content/errorpage.js b/toolkit/components/httpsonlyerror/content/errorpage.js @@ -168,8 +168,17 @@ function clearClickjackingTimeout() { /* Initialize Page */ -initPage(); -// Dispatch this event so tests can detect that we finished loading the error page. -// We're using the same event name as neterror because BrowserTestUtils.sys.mjs relies on that. -let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true }); -document.dispatchEvent(event); +RPMSendQuery("ShouldShowTorConnect").then(shouldShow => { + if (shouldShow) { + // pass orginal destination as redirect param + const encodedRedirect = encodeURIComponent(document.location.href); + document.location.replace(`about:torconnect?redirect=${encodedRedirect}`); + return; + } + + initPage(); + // Dispatch this event so tests can detect that we finished loading the error page. + // We're using the same event name as neterror because BrowserTestUtils.sys.mjs relies on that. + let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true }); + document.dispatchEvent(event); +}); diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build @@ -86,6 +86,7 @@ DIRS += [ "thumbnails", "timermanager", "tooltiptext", + "torconnect", "tor-launcher", "typeaheadfind", "utils", diff --git a/toolkit/components/torconnect/TorConnectChild.sys.mjs b/toolkit/components/torconnect/TorConnectChild.sys.mjs @@ -0,0 +1,85 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +/** + * Actor child class for the about:torconnect page. + * Most of the communication happens through RPM* calls, which do not go through + * this class. + */ +export class TorConnectChild extends RemotePageChild { + /** + * Whether we have redirected the page (after bootstrapping) or not. + * + * @type {boolean} + */ + #redirected = false; + + /** + * If bootstrapping is complete, or TorConnect is disabled, we redirect the + * page. + */ + async #maybeRedirect() { + if (await this.sendQuery("torconnect:should-show")) { + // Enabled and not yet bootstrapped. + return; + } + if (this.#redirected) { + return; + } + this.#redirected = true; + + const redirect = new URLSearchParams( + URL.parse(this.contentWindow.document.location.href)?.search + ).get("redirect"); + + // Fallback in error cases: + let replaceURI = "about:tor"; + const url = URL.parse( + redirect + ? decodeURIComponent(redirect) + : // NOTE: We expect no redirect when address is entered manually, or + // about:torconnect is opened from preferences or urlbar. + // Go to the home page. + await this.sendQuery("torconnect:home-page") + ); + + if (url) { + // Do not allow javascript URI. See tor-browser#41766 + if ( + ["about:", "file:", "https:", "http:"].includes(url.protocol) || + // Allow blank page. See tor-browser#42184. + // Blank page's are given as a chrome URL rather than "about:blank". + url.href === "chrome://browser/content/blanktab.html" + ) { + replaceURI = url.href; + } else { + console.error(`Scheme is not allowed "${redirect}"`); + } + } else { + console.error(`Invalid redirect URL "${redirect}"`); + } + + // Replace the destination to prevent "about:torconnect" entering the + // history. + // NOTE: This is done here, in the window actor, rather than in content + // because we have the privilege to redirect to a "chrome:" uri here (for + // when the HomePage is set to be blank). + this.contentWindow.location.replace(replaceURI); + } + + actorCreated() { + super.actorCreated(); + // about:torconnect could need to be immediately redirected. E.g. if it is + // reached after bootstrapping. + this.#maybeRedirect(); + } + + receiveMessage(message) { + super.receiveMessage(message); + + if (message.name === "torconnect:stage-change") { + this.#maybeRedirect(); + } + } +} diff --git a/toolkit/components/torconnect/TorConnectParent.sys.mjs b/toolkit/components/torconnect/TorConnectParent.sys.mjs @@ -0,0 +1,269 @@ +// Copyright (c) 2021, The Tor Project, Inc. + +import { TorStrings } from "resource://gre/modules/TorStrings.sys.mjs"; +import { + TorConnect, + TorConnectTopics, +} from "resource://gre/modules/TorConnect.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + HomePage: "resource:///modules/HomePage.sys.mjs", +}); + +const userHasEverClickedConnectPref = + "torbrowser.about_torconnect.user_has_ever_clicked_connect"; + +/* +This object is basically a marshalling interface between the TorConnect module +and a particular about:torconnect page +*/ + +/** + * Actor parent class for the about:torconnect page. + * It adapts and relays the messages from and to the TorConnect module. + */ +export class TorConnectParent extends JSWindowActorParent { + constructor(...args) { + super(...args); + + const self = this; + + // JSWindowActiveParent derived objects cannot observe directly, so create a + // member object to do our observing for us. + // + // This object converts the various lifecycle events from the TorConnect + // module, and maintains a state object which we pass down to our + // about:torconnect page, which uses the state object to update its UI. + this.torConnectObserver = { + observe(subject, topic) { + const obj = subject?.wrappedJSObject; + switch (topic) { + case TorConnectTopics.StageChange: + self.sendAsyncMessage("torconnect:stage-change", obj); + break; + case TorConnectTopics.BootstrapProgress: + self.sendAsyncMessage("torconnect:bootstrap-progress", obj); + break; + case TorConnectTopics.QuickstartChange: + self.sendAsyncMessage( + "torconnect:quickstart-change", + TorConnect.quickstart + ); + break; + case TorConnectTopics.RegionNamesChange: + self.sendAsyncMessage("torconnect:region-names-change"); + break; + } + }, + }; + + Services.obs.addObserver( + this.torConnectObserver, + TorConnectTopics.StageChange + ); + Services.obs.addObserver( + this.torConnectObserver, + TorConnectTopics.BootstrapProgress + ); + Services.obs.addObserver( + this.torConnectObserver, + TorConnectTopics.QuickstartChange + ); + Services.obs.addObserver( + this.torConnectObserver, + TorConnectTopics.RegionNamesChange + ); + } + + didDestroy() { + Services.obs.removeObserver( + this.torConnectObserver, + TorConnectTopics.StageChange + ); + Services.obs.removeObserver( + this.torConnectObserver, + TorConnectTopics.BootstrapProgress + ); + Services.obs.removeObserver( + this.torConnectObserver, + TorConnectTopics.QuickstartChange + ); + Services.obs.removeObserver( + this.torConnectObserver, + TorConnectTopics.RegionNamesChange + ); + } + + async receiveMessage(message) { + switch (message.name) { + case "torconnect:should-show": + return Promise.resolve(TorConnect.shouldShowTorConnect); + case "torconnect:home-page": + // If there are multiple home pages, just load the first one. + return Promise.resolve( + TorConnectParent.fixupURIs(lazy.HomePage.get())[0] + ); + case "torconnect:set-quickstart": + TorConnect.quickstart = message.data; + break; + case "torconnect:open-tor-preferences": + this.browsingContext.top.embedderElement.ownerGlobal.openPreferences( + "connection" + ); + break; + case "torconnect:view-tor-logs": + this.browsingContext.top.embedderElement.ownerGlobal.openPreferences( + "connection-viewlogs" + ); + break; + case "torconnect:restart": + Services.startup.quit( + Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit + ); + break; + case "torconnect:start-again": + TorConnect.startAgain(); + break; + case "torconnect:choose-region": + TorConnect.chooseRegion(); + break; + case "torconnect:begin-bootstrapping": + if (message.data.userClickedConnect) { + Services.prefs.setBoolPref(userHasEverClickedConnectPref, true); + } + TorConnect.beginBootstrapping(message.data.regionCode); + break; + case "torconnect:cancel-bootstrapping": + TorConnect.cancelBootstrapping(); + break; + case "torconnect:get-init-args": + // Called on AboutTorConnect.init(), pass down all state data it needs + // to init. + return { + TorStrings, + Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr", + stage: TorConnect.stage, + userHasEverClickedConnect: Services.prefs.getBoolPref( + userHasEverClickedConnectPref, + false + ), + quickstartEnabled: TorConnect.quickstart, + }; + case "torconnect:get-regions": + return TorConnect.getFrequentRegions().then(frequent => { + return { names: TorConnect.getRegionNames(), frequent }; + }); + } + return undefined; + } + + /** + * Open the "about:torconnect" tab. + * + * Bootstrapping can also be automatically triggered at the same time, if the + * current TorConnect stage allows for it. + * + * @param {object} [options] - extra options. + * @param {"soft"|"hard"} [options.beginBootstrapping] - Whether to try and + * begin bootstrapping. "soft" will only trigger the bootstrap if we are not + * `potentiallyBlocked`. "hard" will try begin the bootstrap regardless. + * @param {string} [options.regionCode] - A region to pass in for + * auto-bootstrapping. + */ + static open(options) { + if (!TorConnect.shouldShowTorConnect) { + // Already bootstrapped, so don't reopen about:torconnect. + return; + } + + const win = lazy.BrowserWindowTracker.getTopWindow(); + win.switchToTabHavingURI("about:torconnect", true, { + ignoreQueryString: true, + }); + + if ( + !options?.beginBootstrapping || + (options.beginBootstrapping !== "hard" && + TorConnect.potentiallyBlocked) || + (options.regionCode && !TorConnect.canBeginAutoBootstrap) || + (!options.regionCode && !TorConnect.canBeginNormalBootstrap) + ) { + return; + } + + TorConnect.beginBootstrapping(options.regionCode); + } + + /** + * Convert the given url into an about:torconnect page that redirects to it. + * + * @param {string} url - The url to convert. + * + * @returns {string} - The about:torconnect url. + */ + static getRedirectURL(url) { + return `about:torconnect?redirect=${encodeURIComponent(url)}`; + } + + /** + * Convert the given object into a list of valid URIs. + * + * The object is either from the user's homepage preference (which may + * contain multiple domains separated by "|") or uris passed to the browser + * via command-line. + * + * @param {string|string[]} uriVariant - The string to extract uris from. + * + * @returns {string[]} - The array of uris found. + */ + static fixupURIs(uriVariant) { + let uriArray; + if (typeof uriVariant === "string") { + uriArray = uriVariant.split("|"); + } else if ( + Array.isArray(uriVariant) && + uriVariant.every(entry => typeof entry === "string") + ) { + uriArray = uriVariant; + } else { + // about:tor as safe fallback + console.error(`Received unknown variant '${JSON.stringify(uriVariant)}'`); + uriArray = ["about:tor"]; + } + + // Attempt to convert user-supplied string to a uri, fallback to + // about:tor if cannot convert to valid uri object + return uriArray.map(uriString => { + try { + return ( + Services.uriFixup.getFixupURIInfo( + uriString, + Ci.nsIURIFixup.FIXUP_FLAG_NONE + ).preferredURI?.spec ?? "about:tor" + ); + } catch (e) { + console.error(`Failed to parse ${uriString}`, e); + return "about:tor"; + } + }); + } + + /** + * Replace startup URIs (home pages or command line) with about:torconnect + * URIs which redirect to them after bootstrapping. + * + * @param {string|string[]} uriVariant - The string to extract uris from. + * + * @returns {string[]} - The array or uris to use instead. + */ + static getURIsToLoad(uriVariant) { + const uris = this.fixupURIs(uriVariant); + const localUriRx = /^(file:|moz-extension:)/; + return uris.map(uri => + localUriRx.test(uri) ? uri : this.getRedirectURL(uri) + ); + } +} diff --git a/toolkit/components/torconnect/content/aboutTorConnect.css b/toolkit/components/torconnect/content/aboutTorConnect.css @@ -0,0 +1,272 @@ +/* Copyright (c) 2021, The Tor Project, Inc. */ + +@import url("chrome://global/skin/error-pages.css"); +@import url("chrome://global/skin/tor-colors.css"); +@import url("chrome://global/skin/onion-pattern.css"); + +body:not(.loaded) { + /* Keep blank whilst loading. */ + display: none; +} + +#breadcrumbs { + display: flex; + align-items: center; + margin: 0 0 24px 0; +} + +#breadcrumbs.hidden { + visibility: hidden; +} + +.breadcrumb-item, +.breadcrumb-separator { + display: flex; + margin: 0; + margin-inline-start: 20px; + padding: 8px; +} + +.breadcrumb-item { + align-items: center; + cursor: pointer; + color: var(--text-color); + border-radius: var(--border-radius-small); +} + +.breadcrumb-item:hover { + color: var(--color-accent-primary); + background-color: var(--button-background-color-hover); +} + +.breadcrumb-item:active { + color: var(--color-accent-primary-active); + background-color: var(--button-background-color-active); +} + +.breadcrumb-separator { + width: 15px; + list-style-image: url("chrome://global/content/torconnect/arrow-right.svg"); +} + +.breadcrumb-separator:dir(rtl) { + scale: -1 1; +} + +.breadcrumb-icon { + display: inline list-item; + height: 16px; + list-style-position: inside; + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: currentColor; +} + +.breadcrumb-item.active { + color: var(--color-accent-primary); +} + +.breadcrumb-item.disabled, +.breadcrumb-item.disabled:hover, +.breadcrumb-item.disabled:active { + color: var(--text-color); + opacity: 0.4; + cursor: default; +} + +.breadcrumb-item.hidden, +.breadcrumb-separator.hidden { + display: none; +} + +#tor-connect-heading { + /* Do not show the focus outline. */ + outline: none; +} + +#connect-to-tor { + margin-inline-start: 0; +} + +#connect-to-tor-icon { + list-style-image: url("chrome://global/content/torconnect/tor-connect.svg"); +} + +#connection-assist-icon { + list-style-image: url("chrome://global/content/torconnect/tor-connect-broken.svg"); +} + +#location-settings-icon { + list-style-image: url("chrome://global/content/torconnect/globe.svg"); +} + +#try-bridge { + cursor: default; +} + +#try-bridge-icon { + list-style-image: url("chrome://global/content/torconnect/bridge.svg"); +} + +#locationDropdownLabel { + margin-block: auto; + margin-inline: 4px; +} + +#locationDropdownLabel.error { + color: var(--text-color-error); +} + +/* this follows similar css in error-pages.css for buttons */ +@media only screen and (min-width: 480px) { + form#locationDropdown { + margin-inline: 4px; + /* subtracting out the margin is needeed because by + default forms have different margins than buttons */ + max-width: calc(100% - 8px); + } +} + +@media only screen and (max-width: 480px) { + #tryAgainButton { + margin-top: 4px; + } +} + +form#locationDropdown { + width: 240px; +} + +form#locationDropdown select { + max-width: 100%; + margin-inline: 0; + font-weight: var(--font-weight-bold); +} + +:root { + --progressbar-shadow-start: rgba(255, 255, 255, 0.7); + --progressbar-gradient: linear-gradient(90deg, #fc00ff 0%, #00dbde 50%, #fc00ff 100%); +} + +@media (prefers-color-scheme: dark) { + :root { + --progressbar-shadow-start: rgba(28, 27, 34, 0.7); + } +} + +#progressBar:not([hidden]) { + position: fixed; + inset-block-start: 0; + inset-inline: 0; + display: grid; + --progress-percent: 0%; + --progress-animation: progressAnimation 5s ease infinite; + --progress-bar-height: 7px; +} + +#progressBar > * { + grid-area: 1 / 1; +} + +#progressBarBackground { + width: 100%; + height: var(--progress-bar-height); + background: var(--background-color-box-info); +} + +#progressBackground { + z-index: 1; + width: var(--progress-percent); + height: 66px; + margin-block-start: -26px; + background-image: linear-gradient(var(--progressbar-shadow-start), var(--background-color-canvas) 100%), var(--progressbar-gradient); + animation: var(--progress-animation); + filter: blur(5px); + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-border-radius-tokens */ + border-end-end-radius: 33px; +} + +#progressSolid { + z-index: 2; + width: var(--progress-percent); + height: var(--progress-bar-height); + /* stylelint-disable stylelint-plugin-mozilla/use-border-radius-tokens */ + border-start-end-radius: calc(var(--progress-bar-height) / 2); + border-end-end-radius: calc(var(--progress-bar-height) / 2); + /* stylelint-enable stylelint-plugin-mozilla/use-border-radius-tokens */ + background-image: var(--progressbar-gradient); + animation: var(--progress-animation); +} + +#progressBackground, +#progressSolid { + background-size: 200% 100%; +} + +@keyframes progressAnimation { + 0% { + background-position: 200%; + } + 50% { + background-position: 100%; + } + 100% { + background-position: 0%; + } +} + +@keyframes progressAnimation { + 0% { + background-position: 200%; + } + 50% { + background-position: 100%; + } + 100% { + background-position: 0%; + } +} + +#connectPageContainer { + margin-top: 10vh; + width: 100%; + max-width: 45em; +} + +#quickstartToggle { + width: max-content; +} + +/* mirrors p element spacing */ +#viewLogButton { + margin: 1em 0; +} + +body.aboutTorConnect { + justify-content: space-between; + /* Always reserve 150px for the background, plus 15px padding with content. */ + padding-block-end: calc(var(--onion-pattern-height) + 15px); +} + +body.aboutTorConnect .title { + background-image: url("chrome://global/content/torconnect/tor-connect.svg"); + -moz-context-properties: stroke, fill, fill-opacity; + fill-opacity: 1; + fill: var(--icon-color); + stroke: var(--icon-color); + /* Make sure there is no padding between the title and #breadcrumbs. */ + padding-block-start: 0; +} + +body.aboutTorConnect .title.offline { + background-image: url("chrome://global/content/torconnect/connection-failure.svg"); +} + +body.aboutTorConnect .title:is(.assist, .final) { + background-image: url("chrome://global/content/torconnect/tor-connect-broken.svg"); +} + +body.aboutTorConnect .title.location { + background-image: url("chrome://global/content/torconnect/connection-location.svg"); + stroke: var(--icon-color-warning); +} diff --git a/toolkit/components/torconnect/content/aboutTorConnect.html b/toolkit/components/torconnect/content/aboutTorConnect.html @@ -0,0 +1,90 @@ +<!-- Copyright (c) 2021, The Tor Project, Inc. --> +<!doctype html> +<html> + <head> + <meta + http-equiv="Content-Security-Policy" + content="default-src chrome:; object-src 'none'" + /> + <link + rel="stylesheet" + href="chrome://global/content/torconnect/aboutTorConnect.css" + type="text/css" + media="all" + /> + + <script + type="module" + src="chrome://global/content/elements/moz-toggle.mjs" + ></script> + </head> + <body class="aboutTorConnect onion-pattern-background"> + <div id="progressBar" hidden="hidden"> + <div id="progressSolid"></div> + <div id="progressBackground"></div> + <div id="progressBarBackground"></div> + </div> + <div id="connectPageContainer" class="container"> + <div id="breadcrumbs" class="hidden"> + <span id="connect-to-tor" class="breadcrumb-item"> + <span id="connect-to-tor-icon" class="breadcrumb-icon"></span> + <span class="breadcrumb-label"></span> + </span> + <span + id="connection-assist-separator" + class="breadcrumb-separator breadcrumb-icon" + ></span> + <span id="connection-assist" class="breadcrumb-item"> + <span id="connection-assist-icon" class="breadcrumb-icon"></span> + <span class="breadcrumb-label"></span> + </span> + <span + id="try-bridge-separator" + class="breadcrumb-separator breadcrumb-icon" + ></span> + <span id="try-bridge" class="breadcrumb-item"> + <span id="try-bridge-icon" class="breadcrumb-icon"></span> + <span class="breadcrumb-label"></span> + </span> + </div> + <div id="text-container"> + <div class="title"> + <h1 id="tor-connect-heading" class="title-text" tabindex="-1"></h1> + </div> + <div id="connectLongContent"> + <p id="connectLongContentText"></p> + </div> + <div id="connectShortDesc"> + <p id="connectShortDescText"></p> + </div> + + <button id="viewLogButton"></button> + + <div id="quickstartContainer"> + <moz-toggle id="quickstartToggle"></moz-toggle> + </div> + + <div id="connectButtonContainer" class="button-container"> + <button id="restartButton" hidden="true"></button> + <button id="configureButton" hidden="true"></button> + <button id="cancelButton" hidden="true"></button> + <button id="connectButton" hidden="true" class="tor-button"></button> + <label id="locationDropdownLabel" for="countries"></label> + <form id="locationDropdown" hidden="true"> + <select id="regions-select"> + <option id="first-region-option"></option> + <optgroup id="frequent-regions-option-group"></optgroup> + <optgroup id="full-regions-option-group"></optgroup> + </select> + </form> + <button + id="tryBridgeButton" + hidden="true" + class="tor-button" + ></button> + </div> + </div> + </div> + <script src="chrome://global/content/torconnect/aboutTorConnect.js"></script> + </body> +</html> diff --git a/toolkit/components/torconnect/content/aboutTorConnect.js b/toolkit/components/torconnect/content/aboutTorConnect.js @@ -0,0 +1,936 @@ +// Copyright (c) 2021, The Tor Project, Inc. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// populated in AboutTorConnect.init() +let TorStrings = {}; + +const UIStates = Object.freeze({ + ConnectToTor: "ConnectToTor", + Offline: "Offline", + ConnectionAssist: "ConnectionAssist", + CouldNotLocate: "CouldNotLocate", + LocationConfirm: "LocationConfirm", + FinalError: "FinalError", +}); + +const BreadcrumbStatus = Object.freeze({ + Hidden: "hidden", + Disabled: "disabled", + Default: "default", + Active: "active", +}); + +/** + * The controller for the about:torconnect page. + */ +class AboutTorConnect { + selectors = Object.freeze({ + textContainer: { + title: "div.title", + longContentText: "#connectLongContentText", + }, + progress: { + description: "p#connectShortDescText", + meter: "div#progressBar", + }, + breadcrumbs: { + container: "#breadcrumbs", + connectToTor: { + link: "#connect-to-tor", + label: "#connect-to-tor .breadcrumb-label", + }, + connectionAssist: { + separator: "#connection-assist-separator", + link: "#connection-assist", + label: "#connection-assist .breadcrumb-label", + }, + tryBridge: { + separator: "#try-bridge-separator", + link: "#try-bridge", + label: "#try-bridge .breadcrumb-label", + }, + }, + viewLog: { + button: "#viewLogButton", + }, + quickstart: { + container: "div#quickstartContainer", + toggle: "#quickstartToggle", + }, + buttons: { + restart: "button#restartButton", + configure: "button#configureButton", + cancel: "button#cancelButton", + connect: "button#connectButton", + tryBridge: "button#tryBridgeButton", + locationDropdownLabel: "#locationDropdownLabel", + locationDropdown: "form#locationDropdown", + locationDropdownSelect: "#regions-select", + }, + }); + + elements = Object.freeze({ + title: document.querySelector(this.selectors.textContainer.title), + heading: document.getElementById("tor-connect-heading"), + longContentText: document.querySelector( + this.selectors.textContainer.longContentText + ), + progressDescription: document.querySelector( + this.selectors.progress.description + ), + progressMeter: document.querySelector(this.selectors.progress.meter), + breadcrumbContainer: document.querySelector( + this.selectors.breadcrumbs.container + ), + connectToTorLink: document.querySelector( + this.selectors.breadcrumbs.connectToTor.link + ), + connectToTorLabel: document.querySelector( + this.selectors.breadcrumbs.connectToTor.label + ), + connectionAssistSeparator: document.querySelector( + this.selectors.breadcrumbs.connectionAssist.separator + ), + connectionAssistLink: document.querySelector( + this.selectors.breadcrumbs.connectionAssist.link + ), + connectionAssistLabel: document.querySelector( + this.selectors.breadcrumbs.connectionAssist.label + ), + tryBridgeSeparator: document.querySelector( + this.selectors.breadcrumbs.tryBridge.separator + ), + tryBridgeLink: document.querySelector( + this.selectors.breadcrumbs.tryBridge.link + ), + tryBridgeLabel: document.querySelector( + this.selectors.breadcrumbs.tryBridge.label + ), + viewLogButton: document.querySelector(this.selectors.viewLog.button), + quickstartContainer: document.querySelector( + this.selectors.quickstart.container + ), + quickstartToggle: document.querySelector(this.selectors.quickstart.toggle), + restartButton: document.querySelector(this.selectors.buttons.restart), + configureButton: document.querySelector(this.selectors.buttons.configure), + cancelButton: document.querySelector(this.selectors.buttons.cancel), + connectButton: document.querySelector(this.selectors.buttons.connect), + locationDropdownLabel: document.querySelector( + this.selectors.buttons.locationDropdownLabel + ), + locationDropdown: document.querySelector( + this.selectors.buttons.locationDropdown + ), + locationDropdownSelect: document.querySelector( + this.selectors.buttons.locationDropdownSelect + ), + firstRegionOption: document.getElementById("first-region-option"), + frequentRegionsOptionGroup: document.getElementById( + "frequent-regions-option-group" + ), + fullRegionsOptionGroup: document.getElementById( + "full-regions-option-group" + ), + tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge), + }); + + /** + * The currently shown stage, or `null` if the page in uninitialised. + * + * @type {?string} + */ + shownStage = null; + + /** + * A promise that resolves to a list of region names and frequent regions, or + * `null` if this needs to be re-fetched from the TorConnectParent. + * + * @type {?Promise<object>} + */ + regions = null; + + /** + * The option value that *should* be selected when the list of regions is + * populated. + * + * @type {string} + */ + selectedRegion = ""; + + /** + * Whether the user requested a cancellation of the bootstrap from *this* + * page. + * + * @type {boolean} + */ + userCancelled = false; + + /** + * Start a normal bootstrap attempt. + * + * @param {boolean} userClickedConnect - Whether this request was triggered by + * the user clicking the "Connect" button on the "Start" page. + */ + beginBootstrapping(userClickedConnect) { + RPMSendAsyncMessage("torconnect:begin-bootstrapping", { + userClickedConnect, + }); + } + + /** + * Start an auto bootstrap attempt. + * + * @param {string} regionCode - The region code to use for the bootstrap, or + * "automatic". + */ + beginAutoBootstrapping(regionCode) { + RPMSendAsyncMessage("torconnect:begin-bootstrapping", { + regionCode, + }); + } + + /** + * Try and cancel the current bootstrap attempt. + */ + cancelBootstrapping() { + RPMSendAsyncMessage("torconnect:cancel-bootstrapping"); + this.userCancelled = true; + } + + /* + Element helper methods + */ + + show(element, primary = false) { + element.classList.toggle("primary", primary); + element.removeAttribute("hidden"); + } + + hide(element) { + element.setAttribute("hidden", "true"); + } + + hideButtons() { + this.hide(this.elements.quickstartContainer); + this.hide(this.elements.restartButton); + this.hide(this.elements.configureButton); + this.hide(this.elements.cancelButton); + this.hide(this.elements.connectButton); + this.hide(this.elements.locationDropdownLabel); + this.hide(this.elements.locationDropdown); + this.hide(this.elements.tryBridgeButton); + } + + setTitle(title, className) { + this.elements.heading.textContent = title; + this.elements.title.className = "title"; + if (className) { + this.elements.title.classList.add(className); + } + document.title = title; + } + + setLongText(...args) { + this.elements.longContentText.textContent = ""; + this.elements.longContentText.append(...args); + } + + setBreadcrumbsStatus(connectToTor, connectionAssist, tryBridge) { + this.elements.breadcrumbContainer.classList.remove("hidden"); + const elems = [ + [this.elements.connectToTorLink, connectToTor, null], + [ + this.elements.connectionAssistLink, + connectionAssist, + this.elements.connectionAssistSeparator, + ], + [ + this.elements.tryBridgeLink, + tryBridge, + this.elements.tryBridgeSeparator, + ], + ]; + elems.forEach(([elem, status, separator]) => { + elem.classList.remove(BreadcrumbStatus.Hidden); + elem.classList.remove(BreadcrumbStatus.Disabled); + elem.classList.remove(BreadcrumbStatus.Active); + if (status !== "") { + elem.classList.add(status); + } + separator?.classList.toggle("hidden", status === BreadcrumbStatus.Hidden); + }); + } + + hideBreadcrumbs() { + this.elements.breadcrumbContainer.classList.add("hidden"); + } + + getLocalizedStatus(status) { + const aliases = { + conn_dir: "conn", + handshake_dir: "onehop_create", + conn_or: "enough_dirinfo", + handshake_or: "ap_conn", + }; + if (status in aliases) { + status = aliases[status]; + } + return TorStrings.torConnect.bootstrapStatus[status] ?? status; + } + + getMaybeLocalizedError(error) { + switch (error.code) { + case "Offline": + return TorStrings.torConnect.offline; + case "BootstrapError": { + if (!error.phase || !error.reason) { + return TorStrings.torConnect.torBootstrapFailed; + } + let status = this.getLocalizedStatus(error.phase); + const reason = + TorStrings.torConnect.bootstrapWarning[error.reason] ?? error.reason; + return TorStrings.torConnect.bootstrapFailedDetails + .replace("%1$S", status) + .replace("%2$S", reason); + } + case "CannotDetermineCountry": + return TorStrings.torConnect.cannotDetermineCountry; + case "NoSettingsForCountry": + return TorStrings.torConnect.noSettingsForCountry; + case "AllSettingsFailed": + return TorStrings.torConnect.autoBootstrappingAllFailed; + case "ExternaError": + // A standard JS error, or something for which we do probably do not + // have a translation. Returning the original message is the best we can + // do. + return error.message; + default: + console.warn(`Unknown error code: ${error.code}`, error); + return error.message || error.code; + } + } + + /** + * The connect button that was focused just prior to a bootstrap attempt, if + * any. + * + * @type {?Element} + */ + preBootstrappingFocus = null; + + /** + * The stage that was shown on this page just prior to a bootstrap attempt. + * + * @type {?string} + */ + preBootstrappingStage = null; + + /* + These methods update the UI based on the current TorConnect state + */ + + /** + * Update the shown stage. + * + * @param {ConnectStage} stage - The new stage to show. + * @param {boolean} [focusConnect=false] - Whether to try and focus the + * connect button, if we are in the Start stage. + */ + updateStage(stage, focusConnect = false) { + if (stage.name === this.shownStage) { + return; + } + + const prevStage = this.shownStage; + this.shownStage = stage.name; + // Make a request to change the selected region in the next call to + // selectRegionOption. + this.selectedRegion = stage.defaultRegion; + + // By default we want to reset the focus to the top of the page when + // changing the displayed page since we want a user to read the new page + // before activating a control. + let moveFocus = this.elements.heading; + + if (stage.name === "Bootstrapping") { + this.preBootstrappingStage = prevStage; + this.preBootstrappingFocus = null; + if (focusConnect && stage.isQuickstart) { + // If this is the initial automatic bootstrap triggered by the + // quickstart preference, treat as if the previous shown stage was + // "Start" and the user clicked the "Connect" button. + // Then, if the user cancels, the focus should still move to the + // "Connect" button. + this.preBootstrappingStage = "Start"; + this.preBootstrappingFocus = this.elements.connectButton; + } else if (this.elements.connectButton.contains(document.activeElement)) { + this.preBootstrappingFocus = this.elements.connectButton; + } else if ( + this.elements.tryBridgeButton.contains(document.activeElement) + ) { + this.preBootstrappingFocus = this.elements.tryBridgeButton; + } + } else { + if ( + this.userCancelled && + prevStage === "Bootstrapping" && + stage.name === this.preBootstrappingStage && + this.preBootstrappingFocus && + this.elements.cancelButton.contains(document.activeElement) + ) { + // If returning back to the same stage after the user tried to cancel + // bootstrapping from within this page, then we restore the focus to the + // connect button to allow the user to quickly re-try. + // If the bootstrap was cancelled for any other reason, we reset the + // focus as usual. + moveFocus = this.preBootstrappingFocus; + } + // Clear the Bootstrapping variables. + this.preBootstrappingStage = null; + this.preBootstrappingFocus = null; + } + + // Clear the recording of the cancellation request. + this.userCancelled = false; + + let isLoaded = true; + let showProgress = false; + let showLog = false; + switch (stage.name) { + case "Disabled": + console.error("Should not be open when TorConnect is disabled"); + break; + case "Loading": + // Unexpected for this page to open so early. + console.warn("Page opened whilst loading"); + isLoaded = false; + break; + case "Start": + this.showStart(stage.tryAgain, stage.potentiallyBlocked); + if (focusConnect) { + moveFocus = this.elements.connectButton; + } + break; + case "Bootstrapping": + showProgress = true; + this.showBootstrapping(stage.bootstrapTrigger, stage.tryAgain); + // Always focus the cancel button. + moveFocus = this.elements.cancelButton; + break; + case "Offline": + showLog = true; + this.showOffline(); + break; + case "ChooseRegion": + showLog = true; + this.showChooseRegion(stage.error); + break; + case "RegionNotFound": + showLog = true; + this.showRegionNotFound(); + break; + case "ConfirmRegion": + showLog = true; + this.showConfirmRegion(stage.error); + break; + case "FinalError": + showLog = true; + this.showFinalError(stage.error); + break; + case "Bootstrapped": + showProgress = true; + this.showBootstrapped(); + break; + default: + console.error(`Unknown stage ${stage.name}`); + break; + } + + if (showProgress) { + this.show(this.elements.progressMeter); + } else { + this.hide(this.elements.progressMeter); + } + + this.updateBootstrappingStatus(stage.bootstrappingStatus); + + if (showLog) { + this.show(this.elements.viewLogButton); + } else { + this.hide(this.elements.viewLogButton); + } + + document.body.classList.toggle("loaded", isLoaded); + moveFocus.focus(); + } + + updateBootstrappingStatus(data) { + this.elements.progressMeter.style.setProperty( + "--progress-percent", + `${data.progress}%` + ); + if (this.shownStage === "Bootstrapping" && data.hasWarning) { + // When bootstrapping starts, we hide the log button, but we re-show it if + // we get a warning. + this.show(this.elements.viewLogButton); + } + } + + updateQuickstart(enabled) { + this.elements.quickstartToggle.pressed = enabled; + } + + showBootstrapped() { + this.setTitle(TorStrings.torConnect.torConnected, ""); + this.setLongText(TorStrings.settings.torPreferencesDescription); + this.elements.progressDescription.textContent = ""; + this.hideButtons(); + } + + showStart(tryAgain, potentiallyBlocked) { + this.setTitle(TorStrings.torConnect.torConnect, ""); + this.setLongText(TorStrings.settings.torPreferencesDescription); + this.elements.progressDescription.textContent = ""; + this.hideButtons(); + this.show(this.elements.quickstartContainer); + this.show(this.elements.configureButton); + this.show(this.elements.connectButton, true); + this.elements.connectButton.textContent = tryAgain + ? TorStrings.torConnect.tryAgain + : TorStrings.torConnect.torConnectButton; + if (potentiallyBlocked) { + this.setBreadcrumbsStatus( + BreadcrumbStatus.Active, + BreadcrumbStatus.Default, + BreadcrumbStatus.Disabled + ); + } + } + + showBootstrapping(trigger, tryAgain) { + let title = ""; + let description = ""; + const breadcrumbs = [ + BreadcrumbStatus.Disabled, + BreadcrumbStatus.Disabled, + BreadcrumbStatus.Disabled, + ]; + switch (trigger) { + case "Start": + case "Offline": + breadcrumbs[0] = BreadcrumbStatus.Active; + title = tryAgain + ? TorStrings.torConnect.tryAgain + : TorStrings.torConnect.torConnecting; + description = TorStrings.settings.torPreferencesDescription; + break; + case "ChooseRegion": + breadcrumbs[2] = BreadcrumbStatus.Active; + title = TorStrings.torConnect.tryingBridge; + description = TorStrings.torConnect.assistDescription; + break; + case "RegionNotFound": + breadcrumbs[2] = BreadcrumbStatus.Active; + title = TorStrings.torConnect.tryingBridgeAgain; + description = TorStrings.torConnect.errorLocationDescription; + break; + case "ConfirmRegion": + breadcrumbs[2] = BreadcrumbStatus.Active; + title = TorStrings.torConnect.tryingBridgeAgain; + description = TorStrings.torConnect.isLocationCorrectDescription; + break; + default: + console.warn("Unrecognized bootstrap trigger", trigger); + break; + } + this.setTitle(title, ""); + this.showConfigureConnectionLink(description); + this.elements.progressDescription.textContent = ""; + if (tryAgain) { + this.setBreadcrumbsStatus(...breadcrumbs); + } else { + this.hideBreadcrumbs(); + } + this.hideButtons(); + this.show(this.elements.cancelButton); + } + + showOffline() { + this.setTitle(TorStrings.torConnect.noInternet, "offline"); + this.setLongText(TorStrings.torConnect.noInternetDescription); + this.elements.progressDescription.textContent = + TorStrings.torConnect.offline; + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Active, + BreadcrumbStatus.Hidden + ); + this.hideButtons(); + this.show(this.elements.configureButton); + this.show(this.elements.connectButton, true); + this.elements.connectButton.textContent = TorStrings.torConnect.tryAgain; + } + + showChooseRegion(error) { + this.setTitle(TorStrings.torConnect.couldNotConnect, "assist"); + this.showConfigureConnectionLink(TorStrings.torConnect.assistDescription); + this.elements.progressDescription.textContent = + this.getMaybeLocalizedError(error); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Active, + BreadcrumbStatus.Disabled + ); + this.showLocationForm(true, TorStrings.torConnect.tryBridge); + } + + showRegionNotFound() { + this.setTitle(TorStrings.torConnect.errorLocation, "location"); + this.showConfigureConnectionLink( + TorStrings.torConnect.errorLocationDescription + ); + this.elements.progressDescription.textContent = + TorStrings.torConnect.cannotDetermineCountry; + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Active, + BreadcrumbStatus.Disabled + ); + this.showLocationForm(false, TorStrings.torConnect.tryBridge); + } + + showConfirmRegion(error) { + this.setTitle(TorStrings.torConnect.isLocationCorrect, "location"); + this.showConfigureConnectionLink( + TorStrings.torConnect.isLocationCorrectDescription + ); + this.elements.progressDescription.textContent = + this.getMaybeLocalizedError(error); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Default, + BreadcrumbStatus.Active + ); + this.showLocationForm(false, TorStrings.torConnect.tryAgain); + } + + showFinalError(error) { + this.setTitle(TorStrings.torConnect.finalError, "final"); + this.setLongText(TorStrings.torConnect.finalErrorDescription); + this.elements.progressDescription.textContent = + this.getMaybeLocalizedError(error); + this.setBreadcrumbsStatus( + BreadcrumbStatus.Default, + BreadcrumbStatus.Default, + BreadcrumbStatus.Active + ); + this.hideButtons(); + this.show(this.elements.restartButton); + this.show(this.elements.configureButton, true); + } + + showConfigureConnectionLink(text) { + const pieces = text.split("%S"); + const link = document.createElement("a"); + link.textContent = TorStrings.torConnect.configureConnection; + link.setAttribute("href", "#"); + link.addEventListener("click", e => { + e.preventDefault(); + RPMSendAsyncMessage("torconnect:open-tor-preferences"); + }); + if (pieces.length > 1) { + const first = pieces.shift(); + this.setLongText(first, link, ...pieces); + } else { + this.setLongText(text); + } + } + + /** + * Try and select the region specified in `selectedRegion`. + */ + selectRegionOption() { + // NOTE: If the region appears in both the frequent list and the full list, + // then this will select the region option in + // frequentRegionsOptionGroup, even if the user had prior selected the + // option from fullRegionsOptionGroup. But the overall value should be the + // same. + this.elements.locationDropdownSelect.value = this.selectedRegion; + if (this.elements.locationDropdownSelect.selectedIndex === -1) { + // Select the first, as a fallback. E.g. in RegionNotFound the + // selectedRegion may still be "automatic", but this is no longer + // available. + this.elements.locationDropdownSelect.selectedIndex = 0; + } + this.validateRegion(); + } + + /** + * Ensure that the current selected region is valid for the shown stage. + */ + validateRegion() { + this.elements.tryBridgeButton.toggleAttribute( + "disabled", + !this.elements.locationDropdownSelect.value + ); + } + + /** + * Populate the full list of regions, if necessary. + */ + async populateDelayedRegionOptions() { + if (this.regions) { + // Already populated, or about to populate. + return; + } + + this.regions = RPMSendQuery("torconnect:get-regions"); + const regions = this.regions; + const { names, frequent } = await regions; + + if (regions !== this.regions) { + // Replaced by a new call. + return; + } + + this.setRegionOptions( + this.elements.frequentRegionsOptionGroup, + frequent.map(code => [code, names[code]]) + ); + + this.setRegionOptions( + this.elements.fullRegionsOptionGroup, + Object.entries(names) + ); + + // Now that the list has been re-populated we want to re-select the + // requested region. + this.selectRegionOption(); + } + + /** + * Set the shown region options. + * + * @param {HTMLOptGroupElement} group - The group to set the children of. + * @param {[string, string|undefined][]} regions - The list of region + * key-value entries to fill the group with. The key is the region code and + * the value is the region's localised name. + */ + setRegionOptions(group, regions) { + const regionNodes = regions + .sort(([_code1, name1], [_code2, name2]) => name1.localeCompare(name2)) + .map(([code, name]) => { + const option = document.createElement("option"); + option.value = code; + // If the name is unexpectedly empty or undefined we use the code + // instead. + option.textContent = name || code; + return option; + }); + group.replaceChildren(...regionNodes); + } + + showLocationForm(isChoose, buttonLabel) { + this.hideButtons(); + + this.elements.firstRegionOption.textContent = isChoose + ? TorStrings.torConnect.automatic + : TorStrings.torConnect.selectCountryRegion; + this.elements.firstRegionOption.value = isChoose ? "automatic" : ""; + + // Try and select the region now, prior to waiting for + // populateDelayedRegionOptions. + this.selectRegionOption(); + + // Async fill the rest of the region options, if needed. + this.populateDelayedRegionOptions(); + + this.show(this.elements.locationDropdownLabel); + this.show(this.elements.locationDropdown); + this.elements.locationDropdownLabel.classList.toggle("error", !isChoose); + this.show(this.elements.tryBridgeButton, true); + if (buttonLabel !== undefined) { + this.elements.tryBridgeButton.textContent = buttonLabel; + } + } + + initElements(direction) { + document.documentElement.setAttribute("dir", direction); + + this.elements.connectToTorLink.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:start-again"); + }); + this.elements.connectToTorLabel.textContent = + TorStrings.torConnect.torConnect; + this.elements.connectionAssistLink.addEventListener("click", () => { + if ( + this.elements.connectionAssistLink.classList.contains( + BreadcrumbStatus.Active + ) || + this.elements.connectionAssistLink.classList.contains( + BreadcrumbStatus.Disabled + ) + ) { + return; + } + RPMSendAsyncMessage("torconnect:choose-region"); + }); + this.elements.connectionAssistLabel.textContent = + TorStrings.torConnect.breadcrumbAssist; + this.elements.tryBridgeLabel.textContent = + TorStrings.torConnect.breadcrumbTryBridge; + + this.hide(this.elements.viewLogButton); + this.elements.viewLogButton.textContent = TorStrings.torConnect.viewLog; + this.elements.viewLogButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:view-tor-logs"); + }); + + this.elements.quickstartToggle.addEventListener("toggle", () => { + const quickstart = this.elements.quickstartToggle.pressed; + RPMSendAsyncMessage("torconnect:set-quickstart", quickstart); + }); + this.elements.quickstartToggle.setAttribute( + "label", + TorStrings.settings.quickstartCheckbox + ); + + this.elements.restartButton.textContent = + TorStrings.torConnect.restartTorBrowser; + this.elements.restartButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:restart"); + }); + + this.elements.configureButton.textContent = + TorStrings.torConnect.torConfigure; + this.elements.configureButton.addEventListener("click", () => { + RPMSendAsyncMessage("torconnect:open-tor-preferences"); + }); + + this.elements.cancelButton.textContent = TorStrings.torConnect.cancel; + this.elements.cancelButton.addEventListener("click", () => { + this.cancelBootstrapping(); + }); + + this.elements.connectButton.textContent = + TorStrings.torConnect.torConnectButton; + this.elements.connectButton.addEventListener("click", () => { + // Record as userClickedConnect if we are in the Start stage. + this.beginBootstrapping(this.shownStage === "Start"); + }); + + this.elements.locationDropdownSelect.addEventListener("change", () => { + // Overwrite the stage requested selectedRegion. + // NOTE: This should not fire in response to a programmatic change in + // value. + // E.g. if the user selects a region, then changes locale, we want the + // same region to be re-selected after the option list is rebuilt. + this.selectedRegion = this.elements.locationDropdownSelect.value; + + this.validateRegion(); + }); + + this.elements.locationDropdownLabel.textContent = + TorStrings.torConnect.unblockInternetIn; + + this.elements.frequentRegionsOptionGroup.setAttribute( + "label", + TorStrings.torConnect.frequentLocations + ); + this.elements.fullRegionsOptionGroup.setAttribute( + "label", + TorStrings.torConnect.otherLocations + ); + + this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge; + this.elements.tryBridgeButton.addEventListener("click", () => { + const value = this.elements.locationDropdownSelect.value; + if (value) { + this.beginAutoBootstrapping(value); + } + }); + + // Prevent repeat triggering on keydown when the Enter key is held down. + // + // Without this, holding down Enter will continue to trigger the button's + // click event until the user stops holding. This means that a user can + // accidentally re-trigger a button several times. And if focus moves to a + // new button it can also get triggered, despite not receiving the initial + // keydown event. + // + // E.g. If the user presses down Enter on the "Connect" button it will + // trigger and focus will move to the "Cancel" button. This should prevent + // the user accidentally triggering the "Cancel" button if they hold down + // Enter for a little bit too long. + for (const button of document.body.querySelectorAll("button")) { + button.addEventListener("keydown", event => { + // If the keydown is a repeating Enter event, ignore it. + // NOTE: If firefox uses wayland display (rather than xwayland), the + // "repeat" event is always "false" so this will not work. + // See bugzilla bug 1784438. Also see bugzilla bug 1594003. + // Currently tor browser uses xwayland by default on linux. + if (event.key === "Enter" && event.repeat) { + event.preventDefault(); + } + }); + } + } + + initObservers() { + // TorConnectParent feeds us state blobs to we use to update our UI + RPMAddMessageListener("torconnect:stage-change", ({ data }) => { + this.updateStage(data); + }); + RPMAddMessageListener("torconnect:bootstrap-progress", ({ data }) => { + this.updateBootstrappingStatus(data); + }); + RPMAddMessageListener("torconnect:quickstart-change", ({ data }) => { + this.updateQuickstart(data); + }); + RPMAddMessageListener("torconnect:region-names-change", () => { + // Reset the regions list. + this.regions = null; + if (!this.elements.locationDropdown.hidden) { + // Re-populate immediately. + this.populateDelayedRegionOptions(); + } + // Else, wait until we show the region select to re-populate. + }); + } + + initKeyboardShortcuts() { + document.onkeydown = evt => { + // unfortunately it looks like we still haven't standardized keycodes to + // integers, so we must resort to a string compare here :( + // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for relevant documentation + if (evt.code === "Escape") { + this.cancelBootstrapping(); + } + }; + } + + async init() { + let args = await RPMSendQuery("torconnect:get-init-args"); + + // various constants + TorStrings = Object.freeze(args.TorStrings); + + this.initElements(args.Direction); + this.initObservers(); + this.initKeyboardShortcuts(); + + // If we have previously opened about:torconnect and the user tried the + // "Connect" button we want to focus the "Connect" button for easy + // activation. + // Otherwise, we do not want to focus it for first time users so they can + // read the full page first. + const focusConnect = args.userHasEverClickedConnect; + this.updateStage(args.stage, focusConnect); + this.updateQuickstart(args.quickstartEnabled); + } +} + +const aboutTorConnect = new AboutTorConnect(); +aboutTorConnect.init(); diff --git a/toolkit/components/torconnect/content/arrow-right.svg b/toolkit/components/torconnect/content/arrow-right.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M10.9991 8.352L5.53406 13.818C5.41557 13.9303 5.25792 13.9918 5.09472 13.9895C4.93152 13.9872 4.77567 13.9212 4.66039 13.8057C4.54511 13.6902 4.47951 13.5342 4.47758 13.3709C4.47565 13.2077 4.53754 13.0502 4.65006 12.932L9.58506 7.998L4.65106 3.067C4.53868 2.94864 4.47697 2.79106 4.47909 2.62786C4.48121 2.46466 4.54698 2.30874 4.66239 2.19333C4.7778 2.07792 4.93372 2.01215 5.09692 2.01003C5.26012 2.00792 5.41769 2.06962 5.53606 2.182L11.0001 7.647L10.9991 8.352Z" fill="context-fill"/> +</svg> diff --git a/toolkit/components/torconnect/content/bridge.svg b/toolkit/components/torconnect/content/bridge.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M1 9.48528C1 9.48528 3.82843 9.48528 6.65685 6.65685C9.48528 3.82843 9.48528 1 9.48528 1" stroke="context-fill" stroke-width="1.25" stroke-linecap="round"/> + <path d="M6.65686 15.1421C6.65686 15.1421 6.65686 12.3137 9.48529 9.48529C12.3137 6.65686 15.1421 6.65686 15.1421 6.65686" stroke="context-fill" stroke-width="1.25" stroke-linecap="round"/> +</svg> diff --git a/toolkit/components/torconnect/content/connection-failure.svg b/toolkit/components/torconnect/content/connection-failure.svg @@ -0,0 +1,7 @@ +<svg fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"> + <rect height="1.22295" rx=".611475" transform="matrix(-.707107 -.707107 -.707107 .707107 14.9464 14.1921)" width="18.6565" fill="context-fill" /> + <g fill="context-fill" fill-rule="evenodd"> + <path d="m3.9498 1.68761c1.19924-.769433 2.60316-1.18761 4.0502-1.18761 1.98912 0 3.8968.79018 5.3033 2.1967s2.1967 3.31418 2.1967 5.3033c0 1.44704-.4182 2.851-1.1876 4.0502l-.9179-.9179c.4428-.7667.7192-1.62112.8075-2.5073h-2.229c-.0215.32648-.0599.65056-.1148.971l-2.22101-2.221h1.08181c-.146-1.92835-.96795-3.74329-2.321-5.125h-.796c-.52113.53217-.96348 1.12861-1.31877 1.77104l-.9115-.9115c.17951-.29615.37579-.58315.58827-.85954h-.408c-.23805.10224-.46875.21884-.69093.34888zm9.0744 2.61245c-.6605-.90129-1.5504-1.60918-2.5772-2.05006h-.407c1.1378 1.47922 1.8109 3.26288 1.934 5.125h2.229c-.1112-1.11187-.5182-2.17365-1.1788-3.07494z" /> + <path d="m3.08673 2.33343c-.13383.11605-.26395.23718-.39003.36327-1.40652 1.40652-2.1967 3.31418-2.1967 5.3033s.79018 3.8968 2.1967 5.3033 3.31418 2.1967 5.3033 2.1967 3.8968-.7902 5.3033-2.1967c.1261-.1261.2472-.2562.3633-.39l-.8933-.8933c-.6277.7494-1.4237 1.3427-2.3253 1.73h-.409c.6819-.8861 1.1969-1.8815 1.5266-2.9377l-1.0208-1.02083c-.3313 1.48633-1.07154 2.85943-2.1478 3.95853h-.795c-1.35305-1.3817-2.175-3.1966-2.321-5.125h4.09731l-1.25-1.25h-2.84731c.06073-.80217.23844-1.58472.52269-2.32462l-.95329-.95329c-.46748 1.02882-.74878 2.13867-.8244 3.27791h-2.229c.11102-1.1118.51787-2.17355 1.17823-3.07484.29198-.39851.62881-.75922 1.00248-1.07575zm-.11176 9.36657c.66039.9014 1.55027 1.6092 2.57703 2.05h.408c-1.13778-1.4792-1.81088-3.2629-1.934-5.125h-2.229c.11081 1.11186.51758 2.1737 1.17797 3.075z" /> + </g> +</svg> diff --git a/toolkit/components/torconnect/content/connection-location.svg b/toolkit/components/torconnect/content/connection-location.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M7.73334 11.442C7.73334 9.541 9.34511 8.00001 11.3333 8.00001C13.3216 8.00001 14.9333 9.541 14.9333 11.442C14.9333 12.801 13.0948 14.729 12.0456 15.7185C11.855 15.899 11.5994 16 11.3333 16C11.0672 16 10.8117 15.899 10.6211 15.7185C9.57191 14.729 7.73334 12.8005 7.73334 11.442ZM10.7466 9.98339C10.9326 9.90634 11.132 9.86667 11.3333 9.86667C11.74 9.86667 12.13 10.0282 12.4176 10.3158C12.7051 10.6033 12.8667 10.9933 12.8667 11.4C12.8667 11.8067 12.7051 12.1967 12.4176 12.4842C12.13 12.7718 11.74 12.9333 11.3333 12.9333C11.132 12.9333 10.9326 12.8937 10.7466 12.8166C10.5605 12.7396 10.3915 12.6266 10.2491 12.4842C10.1067 12.3419 9.99378 12.1728 9.91672 11.9868C9.83966 11.8008 9.8 11.6014 9.8 11.4C9.8 11.1986 9.83966 10.9993 9.91672 10.8132C9.99378 10.6272 10.1067 10.4582 10.2491 10.3158C10.3915 10.1734 10.5605 10.0604 10.7466 9.98339Z" fill="context-stroke"/> + <path d="M15.4042 9.15991C15.489 8.70846 15.5333 8.24275 15.5333 7.76667C15.5333 3.62454 12.1755 0.26667 8.03332 0.26667C3.89119 0.26667 0.533325 3.62454 0.533325 7.76667C0.533325 11.9088 3.89119 15.2667 8.03332 15.2667C8.33067 15.2667 8.62398 15.2494 8.91229 15.2157C7.77562 13.9767 6.66666 12.467 6.66666 11.2932C6.66666 10.0893 7.18892 8.99941 8.03333 8.21045V5.93855C8.83962 5.93855 9.52399 6.46053 9.76697 7.185C10.1327 7.06327 10.5195 6.98297 10.9209 6.95013C10.5653 5.69011 9.40713 4.76667 8.03333 4.76667L8.03332 3.68854C10.0174 3.68854 11.6705 5.10537 12.0361 6.98245C12.4679 7.04338 12.8804 7.1596 13.2649 7.32314C13.0397 4.63121 10.7834 2.51667 8.03332 2.51666V1.43855C11.5283 1.43855 14.3615 4.27174 14.3615 7.76667C14.3615 7.83574 14.3604 7.90454 14.3582 7.97308C14.7773 8.30672 15.1325 8.70843 15.4042 9.15991Z" fill="context-fill"/> +</svg> diff --git a/toolkit/components/torconnect/content/tor-connect-broken.svg b/toolkit/components/torconnect/content/tor-connect-broken.svg @@ -0,0 +1,11 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M3.32745 2.13476C4.60904 1.11242 6.23317 0.501331 8 0.501331C12.1414 0.501331 15.4987 3.85866 15.4987 8C15.4987 9.76709 14.8876 11.3911 13.8652 12.6725L13.0315 11.8388C13.8448 10.7747 14.328 9.44438 14.328 8C14.328 4.50401 11.496 1.672 8 1.672C6.5562 1.672 5.22564 2.15503 4.16105 2.96836L3.32745 2.13476Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M2.35636 3.06235C1.20135 4.38144 0.501343 6.10899 0.501343 8C0.501343 11.5867 3.01868 14.584 6.38401 15.3227C6.73601 15.4 7.09333 15.4533 7.46134 15.4773V9.74933C6.71467 9.52 6.17068 8.82401 6.17068 8C6.17068 7.67615 6.25474 7.37202 6.40223 7.10822L5.55539 6.26138C5.20574 6.75196 5.00001 7.3521 5.00001 8C5.00001 9.06133 5.55201 9.99466 6.38401 10.528V14.1173C3.67201 13.4053 1.67201 10.9387 1.67201 8C1.67201 6.43179 2.24187 4.99718 3.18588 3.89187L2.35636 3.06235Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M6.56041 5.36771L7.44804 6.25534C7.62219 6.20033 7.80762 6.17067 8.00001 6.17067C9.01067 6.17067 9.82934 6.98934 9.82934 8C9.82934 8.19242 9.79968 8.37785 9.7447 8.552L10.6324 9.43967C10.8667 9.01221 11 8.52156 11 8C11 6.34399 9.65601 5 8.00001 5C7.47845 5 6.98783 5.13332 6.56041 5.36771Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M9.73889 10.4449L8.89214 9.59813C8.78095 9.66036 8.6626 9.71127 8.53868 9.74933V15.4773C8.90668 15.4533 9.26401 15.4 9.61601 15.3227C10.8695 15.0475 12.0054 14.459 12.9374 13.6434L12.1076 12.8136C11.396 13.4207 10.5481 13.8726 9.61601 14.1173V10.528C9.65768 10.5013 9.69865 10.4736 9.73889 10.4449Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M12.2609 11.0682C12.8837 10.2055 13.2507 9.14573 13.2507 8C13.2507 5.10133 10.8987 2.74933 7.99999 2.74933C6.85488 2.74933 5.79508 3.11639 4.9319 3.73921L5.77475 4.58207C6.41445 4.16498 7.1787 3.92267 7.99999 3.92267C10.2533 3.92267 12.0773 5.74666 12.0773 8C12.0773 8.82056 11.8348 9.58497 11.4175 10.2248L12.2609 11.0682Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M10.5086 11.2146L11.3423 12.0483C10.8375 12.4651 10.2534 12.7892 9.616 12.9947V11.744C9.93702 11.6057 10.2367 11.4271 10.5086 11.2146Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M4.78492 5.49092L3.95137 4.65737C3.20058 5.56555 2.74933 6.73033 2.74933 8C2.74933 10.336 4.27467 12.3147 6.384 12.9947V11.744C4.936 11.12 3.92267 9.67733 3.92267 8C3.92267 7.05341 4.24455 6.18259 4.78492 5.49092Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M7.16918 7.8752L8.12478 8.83079C8.08406 8.83686 8.04238 8.84 7.99997 8.84C7.53605 8.84 7.15997 8.46392 7.15997 8C7.15997 7.95759 7.16312 7.91592 7.16918 7.8752Z" fill="context-fill" fill-opacity="context-fill-opacity" /> + <path d="M1.15533 1.85684L14.0906 14.7921C14.3511 15.0527 14.3511 15.4751 14.0906 15.7357L14.0906 15.7357C13.83 15.9963 13.4075 15.9963 13.1469 15.7357L0.211679 2.8005C-0.048903 2.53992 -0.0489032 2.11743 0.211682 1.85684C0.472265 1.59626 0.894753 1.59626 1.15533 1.85684Z" fill="context-stroke" fill-opacity="context-fill-opacity" /> +</svg> diff --git a/toolkit/components/torconnect/content/tor-connect.svg b/toolkit/components/torconnect/content/tor-connect.svg @@ -0,0 +1,7 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 0.501331C3.85867 0.501331 0.501343 3.85866 0.501343 8C0.501343 11.5867 3.01868 14.584 6.38401 15.3227C6.73601 15.4 7.09333 15.4533 7.46134 15.4773V9.74933C6.71467 9.52 6.17068 8.82401 6.17068 8C6.17068 6.98934 6.98935 6.17067 8.00001 6.17067C9.01067 6.17067 9.82934 6.98934 9.82934 8C9.82934 8.82401 9.28534 9.52 8.53868 9.74933V15.4773C8.90668 15.4533 9.26401 15.4 9.61601 15.3227C12.9813 14.584 15.4987 11.5867 15.4987 8C15.4987 3.85866 12.1414 0.501331 8.00001 0.501331ZM9.61601 14.1173V10.528C10.448 9.99466 11 9.06133 11 8C11 6.344 9.65601 5 8.00001 5C6.344 5 5.00001 6.344 5.00001 8C5.00001 9.06133 5.55201 9.99466 6.38401 10.528V14.1173C3.67201 13.4053 1.67201 10.9387 1.67201 8C1.67201 4.504 4.50401 1.67201 8.00001 1.67201C11.496 1.67201 14.328 4.50401 14.328 8C14.328 10.9387 12.328 13.4053 9.61601 14.1173Z" fill="context-fill" fill-opacity="context-fill-opacity"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99999 2.74933C5.10133 2.74933 2.74933 5.10134 2.74933 8C2.74933 10.336 4.27467 12.3147 6.384 12.9947V11.744C4.936 11.12 3.92267 9.67733 3.92267 8C3.92267 5.74666 5.74666 3.92267 7.99999 3.92267C10.2533 3.92267 12.0773 5.74666 12.0773 8C12.0773 9.67733 11.064 11.12 9.61599 11.744V12.9947C11.7253 12.3147 13.2507 10.336 13.2507 8C13.2507 5.10133 10.8987 2.74933 7.99999 2.74933Z" fill="context-fill" fill-opacity="context-fill-opacity"/> +<path d="M7.99997 8.84C8.46389 8.84 8.83997 8.46392 8.83997 8C8.83997 7.53608 8.46389 7.16 7.99997 7.16C7.53605 7.16 7.15997 7.53608 7.15997 8C7.15997 8.46392 7.53605 8.84 7.99997 8.84Z" fill="context-fill" fill-opacity="context-fill-opacity"/> +</g> +</svg> diff --git a/toolkit/components/torconnect/content/tor-not-connected-to-connected-animated.svg b/toolkit/components/torconnect/content/tor-not-connected-to-connected-animated.svg @@ -0,0 +1,34 @@ +<svg width="176" height="16" viewBox="0 0 176 16" xmlns="http://www.w3.org/2000/svg"> + <!-- First frame, same as tor-connect-broken.svg --> + <path d="M3.32745 2.13476C4.60904 1.11242 6.23317 0.501331 8 0.501331C12.1414 0.501331 15.4987 3.85866 15.4987 8C15.4987 9.76709 14.8876 11.3911 13.8652 12.6725L13.0315 11.8388C13.8448 10.7747 14.328 9.44438 14.328 8C14.328 4.50401 11.496 1.672 8 1.672C6.5562 1.672 5.22564 2.15503 4.16105 2.96836L3.32745 2.13476Z" fill="context-fill" /> + <path d="M2.35636 3.06235C1.20135 4.38144 0.501343 6.10899 0.501343 8C0.501343 11.5867 3.01868 14.584 6.38401 15.3227C6.73601 15.4 7.09333 15.4533 7.46134 15.4773V9.74933C6.71467 9.52 6.17068 8.82401 6.17068 8C6.17068 7.67615 6.25474 7.37202 6.40223 7.10822L5.55539 6.26138C5.20574 6.75196 5.00001 7.3521 5.00001 8C5.00001 9.06133 5.55201 9.99466 6.38401 10.528V14.1173C3.67201 13.4053 1.67201 10.9387 1.67201 8C1.67201 6.43179 2.24187 4.99718 3.18588 3.89187L2.35636 3.06235Z" fill="context-fill" /> + <path d="M6.56041 5.36771L7.44804 6.25534C7.62219 6.20033 7.80762 6.17067 8.00001 6.17067C9.01067 6.17067 9.82934 6.98934 9.82934 8C9.82934 8.19242 9.79968 8.37785 9.7447 8.552L10.6324 9.43967C10.8667 9.01221 11 8.52156 11 8C11 6.34399 9.65601 5 8.00001 5C7.47845 5 6.98783 5.13332 6.56041 5.36771Z" fill="context-fill" /> + <path d="M9.73889 10.4449L8.89214 9.59813C8.78095 9.66036 8.6626 9.71127 8.53868 9.74933V15.4773C8.90668 15.4533 9.26401 15.4 9.61601 15.3227C10.8695 15.0475 12.0054 14.459 12.9374 13.6434L12.1076 12.8136C11.396 13.4207 10.5481 13.8726 9.61601 14.1173V10.528C9.65768 10.5013 9.69865 10.4736 9.73889 10.4449Z" fill="context-fill" /> + <path d="M12.2609 11.0682C12.8837 10.2055 13.2507 9.14573 13.2507 8C13.2507 5.10133 10.8987 2.74933 7.99999 2.74933C6.85488 2.74933 5.79508 3.11639 4.9319 3.73921L5.77475 4.58207C6.41445 4.16498 7.1787 3.92267 7.99999 3.92267C10.2533 3.92267 12.0773 5.74666 12.0773 8C12.0773 8.82056 11.8348 9.58497 11.4175 10.2248L12.2609 11.0682Z" fill="context-fill" /> + <path d="M10.5086 11.2146L11.3423 12.0483C10.8375 12.4651 10.2534 12.7892 9.616 12.9947V11.744C9.93702 11.6057 10.2367 11.4271 10.5086 11.2146Z" fill="context-fill" /> + <path d="M4.78492 5.49092L3.95137 4.65737C3.20058 5.56555 2.74933 6.73033 2.74933 8C2.74933 10.336 4.27467 12.3147 6.384 12.9947V11.744C4.936 11.12 3.92267 9.67733 3.92267 8C3.92267 7.05341 4.24455 6.18259 4.78492 5.49092Z" fill="context-fill" /> + <path d="M7.16918 7.8752L8.12478 8.83079C8.08406 8.83686 8.04238 8.84 7.99997 8.84C7.53605 8.84 7.15997 8.46392 7.15997 8C7.15997 7.95759 7.16312 7.91592 7.16918 7.8752Z" fill="context-fill" /> + <path d="M1.15533 1.85684L14.0906 14.7921C14.3511 15.0527 14.3511 15.4751 14.0906 15.7357L14.0906 15.7357C13.83 15.9963 13.4075 15.9963 13.1469 15.7357L0.211679 2.8005C-0.048903 2.53992 -0.0489032 2.11743 0.211682 1.85684C0.472265 1.59626 0.894753 1.59626 1.15533 1.85684Z" fill="context-stroke" /> + <!-- End of first frame. --> + <path d="m 26.5604,5.36771 0.8877,0.88763 C 27.6222,6.20033 27.8076,6.17067 28,6.17067 c 1.0107,0 1.8294,0.81867 1.8294,1.82933 0,0.1924 -0.0297,0.3779 -0.0847,0.552 l 0.8877,0.8877 C 30.8667,9.0122 31,8.5216 31,8 31,6.34399 29.656,5 28,5 27.4785,5 26.9878,5.13332 26.5604,5.36771 Z" fill="context-fill" /> + <path d="M 32.2609,11.0682 C 32.8837,10.2055 33.2507,9.1457 33.2507,8 33.2507,5.10133 30.8987,2.74933 28,2.74933 c -1.1451,0 -2.2049,0.36706 -3.0681,0.98988 l 0.8428,0.84286 c 0.6397,-0.41709 1.404,-0.6594 2.2253,-0.6594 2.2533,0 4.0773,1.82399 4.0773,4.07733 0,0.8206 -0.2425,1.585 -0.6598,2.2248 z" fill="context-fill" /> + <path fill-rule="evenodd" d="M 25.1667,1.05506 C 26.0409,0.69808 26.9975,0.50133 28,0.50133 c 4.1414,0 7.4987,3.35732 7.4987,7.49867 0,1.7671 -0.6111,3.3911 -1.6335,4.6725 L 33.0315,11.8388 C 33.8448,10.7747 34.328,9.4444 34.328,8 34.328,4.504 31.496,1.67199 28,1.67199 c -1.4438,0 -2.7744,0.48303 -3.8389,1.29636 L 24.1597,2.96703 c -0.3568,0.27263 -0.6838,0.58239 -0.9752,0.9235 l 0.0014,0.00134 C 22.2419,4.99718 21.672,6.43179 21.672,8 c 0,1.7592 0.7167,3.3492 1.8739,4.4949 0.473,0.4681 1.0196,0.862 1.6208,1.1628 0.385,0.1928 0.7924,0.3481 1.2173,0.4596 V 10.528 C 25.552,9.9947 25,9.0613 25,8 25,7.8685 25.0085,7.7389 25.0249,7.6118 L 24.1287,6.71563 C 23.995,7.11937 23.9227,7.5512 23.9227,8 c 0,1.6773 1.0133,3.12 2.4613,3.744 v 1.2507 C 24.2747,12.3147 22.7493,10.336 22.7493,8 c 0,-0.78053 0.1706,-1.52142 0.4764,-2.18742 L 22.8632,5.45009 c -0.2597,-0.2597 -0.2597,-0.68075 0,-0.94045 0.2597,-0.2597 0.6808,-0.2597 0.9405,0 L 34.0943,14.8002 c 0.2597,0.2597 0.2597,0.6808 0,0.9405 -0.2597,0.2597 -0.6808,0.2597 -0.9405,0 L 31.849,14.4359 c -0.6815,0.4082 -1.4333,0.7112 -2.233,0.8868 -0.352,0.0773 -0.7093,0.1306 -1.0773,0.1546 v -4.3518 l -1.0774,-1.0773 v 5.4291 C 27.0933,15.4533 26.736,15.4 26.384,15.3227 24.9758,15.0136 23.7161,14.309 22.7272,13.3313 21.3519,11.9723 20.5,10.0852 20.5,7.9987 20.5,4.85935 22.4292,2.17055 25.1667,1.05319 Z M 29.616,14.1173 v -1.9144 0.7918 c 0.1953,-0.063 0.3857,-0.1371 0.5702,-0.2216 l 0.804,0.804 c -0.4306,0.2309 -0.8911,0.4134 -1.3742,0.5402 z" fill="context-fill" /> + <path d="m 46.5604,5.36771 0.8877,0.88763 C 47.6222,6.20033 47.8076,6.17067 48,6.17067 c 1.0107,0 1.8294,0.81867 1.8294,1.82933 0,0.1924 -0.0297,0.3779 -0.0847,0.552 l 0.8877,0.8877 C 50.8667,9.0122 51,8.5216 51,8 51,6.34399 49.656,5 48,5 47.4785,5 46.9878,5.13332 46.5604,5.36771 Z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 48,0.50133 c -1.0025,0 -1.9591,0.19675 -2.8333,0.55373 V 1.05319 C 42.4292,2.17055 40.5,4.85935 40.5,7.9987 c 0,2.0865 0.8519,3.9736 2.2272,5.3326 0.9889,0.9777 2.2486,1.6823 3.6568,1.9914 0.352,0.0773 0.7093,0.1306 1.0773,0.1546 v -5.4291 l 1.0774,1.0773 v 4.3518 c 0.368,-0.024 0.7253,-0.0773 1.0773,-0.1546 0.7997,-0.1756 1.5515,-0.4786 2.233,-0.8868 l 1.3048,1.3048 c 0.2597,0.2597 0.6808,0.2597 0.9405,0 0.2597,-0.2597 0.2597,-0.6808 0,-0.9405 L 45.5277,6.23361 c -0.2597,-0.2597 -0.6808,-0.2597 -0.9405,0 -0.2597,0.2597 -0.2597,0.68075 0,0.94045 L 45.0249,7.6118 C 45.0085,7.7389 45,7.8684 45,8 c 0,1.0613 0.552,1.9947 1.384,2.528 v 1.2164 c -1.4476,-0.6242 -2.4607,-2.0667 -2.4607,-3.7437 0,-2.25337 1.824,-4.07736 4.0774,-4.07736 0.0919,0 0.1832,0.00304 0.2736,0.00902 2.1252,0.1409 3.803,1.90649 3.803,4.06764 0,0.8206 -0.2425,1.585 -0.6598,2.2248 l 0.8434,0.8434 C 52.8837,10.2055 53.2507,9.1457 53.2507,8 53.2507,5.10133 50.8987,2.74933 48,2.74933 c -1.1451,0 -2.2049,0.36706 -3.0681,0.98988 C 43.6101,4.69295 42.75,6.24712 42.75,8.0007 c 0,2.3357 1.525,4.3143 3.634,4.9945 v 1.1221 C 45.9591,14.0058 45.5517,13.8511 45.1667,13.6584 44.5655,13.3575 44.0189,12.963 43.5459,12.4949 42.3887,11.3492 41.672,9.7592 41.672,8 c 0,-1.56821 0.5699,-3.00282 1.5139,-4.10813 L 43.1845,3.89053 c 0.2914,-0.34111 0.6184,-0.65087 0.9752,-0.9235 l 0.0014,0.00132 C 45.2256,2.15502 46.5562,1.67199 48,1.67199 c 3.496,0 6.328,2.83201 6.328,6.32801 0,1.4444 -0.4832,2.7747 -1.2965,3.8388 l 0.8337,0.8337 C 54.8876,11.3911 55.4987,9.7671 55.4987,8 55.4987,3.85865 52.1414,0.50133 48,0.50133 Z m 1.616,11.70147 v 1.9145 c 0.4831,-0.1268 0.9436,-0.3093 1.3742,-0.5402 l -0.8039,-0.804 c -0.1846,0.0845 -0.375,0.1586 -0.5703,0.2216 z" fill="context-fill" /> + <path d="m 48,8.21 c 0.116,0 0.21,-0.094 0.21,-0.21 0,-0.116 -0.094,-0.21 -0.21,-0.21 -0.116,0 -0.21,0.094 -0.21,0.21 0,0.116 0.094,0.21 0.21,0.21 z" fill="context-fill" /> + <path fill-rule="evenodd" d="M 67.9987,0.5 H 68 v 0.00133 c 4.1414,0 7.4987,3.35732 7.4987,7.49867 0,1.7671 -0.6111,3.3911 -1.6335,4.6725 L 73.0315,11.8388 C 73.8448,10.7747 74.328,9.4444 74.328,8 74.328,4.504 71.496,1.67199 68,1.67199 c -1.4438,0 -2.7744,0.48303 -3.8389,1.29636 L 64.1597,2.96703 c -1.5132,1.15605 -2.489,2.97944 -2.489,5.03167 0,2.204 1.125,4.1425 2.8316,5.2743 0.5689,0.3773 1.2024,0.665 1.8804,0.843 V 12.9947 C 64.2744,12.3141 62.75,10.3359 62.75,8.0007 62.75,6.24712 63.6107,4.69363 64.9326,3.73989 65.7957,3.11707 66.8549,2.74933 68,2.74933 c 2.8987,0 5.2507,2.352 5.2507,5.25067 0,1.1457 -0.367,2.2055 -0.9898,3.0682 L 71.4175,10.2248 C 71.8348,9.585 72.0773,8.8206 72.0773,8 c 0,-2.16115 -1.6778,-3.92736 -3.803,-4.06827 -0.0904,-0.00598 -0.1817,-0.00839 -0.2736,-0.00839 -2.2534,0 -4.0774,1.82399 -4.0774,4.07736 0,1.6765 1.0124,3.1186 2.4594,3.7431 v -1.2171 c -0.832,-0.5334 -1.384,-1.4667 -1.384,-2.528 0,-1.65603 1.344,-3.00003 3,-3.00003 H 68 V 5 c 1.656,0 3,1.34399 3,3 0,0.5216 -0.1333,1.0122 -0.3676,1.4397 L 69.7447,8.552 C 69.7997,8.3778 69.8294,8.1924 69.8294,8 69.8294,6.98934 69.0107,6.17067 68,6.17067 c -0.1924,0 -0.3778,0.02966 -0.5519,0.08467 L 67.4467,6.254 c -0.7407,0.23396 -1.2774,0.92639 -1.2774,1.7447 0,0.3258 0.0851,0.6317 0.2342,0.8966 -0.0799,-0.2328 -0.027,-0.501 0.1587,-0.6867 0.2597,-0.2597 0.6807,-0.2597 0.9404,0 l 6.5917,6.5916 c 0.2597,0.2597 0.2597,0.6808 0,0.9405 -0.2597,0.2597 -0.6808,0.2597 -0.9405,0 L 71.849,14.4359 c -0.6815,0.4082 -1.4333,0.7112 -2.233,0.8868 -0.352,0.0773 -0.7093,0.1306 -1.0773,0.1546 V 11.1255 L 67.46,10.0469 V 15.476 C 67.092,15.452 66.7347,15.3987 66.3827,15.3214 65.9631,15.2293 65.5567,15.1021 65.1667,14.943 62.4288,13.8261 60.5,11.1382 60.5,7.9987 60.5,4.89266 62.3885,2.22766 65.0799,1.08929 65.977,0.70983 66.9633,0.5 67.9987,0.5 Z m 1.6173,11.7028 v 1.9145 c 0.4831,-0.1268 0.9436,-0.3093 1.3742,-0.5402 l -0.804,-0.804 c -0.1845,0.0845 -0.3749,0.1586 -0.5702,0.2216 z" fill="context-fill" /> + <path d="m 68,8.42 c 0.2319,0 0.42,-0.188 0.42,-0.42 0,-0.232 -0.1881,-0.42 -0.42,-0.42 -0.232,0 -0.42,0.188 -0.42,0.42 0,0.232 0.188,0.42 0.42,0.42 z" fill="context-fill" /> + <path d="M 87.9987,0.5 H 88 v 0.00133 c 4.1414,0 7.4987,3.35732 7.4987,7.49867 0,1.7671 -0.6111,3.3911 -1.6335,4.6725 L 93.0315,11.8388 C 93.8448,10.7747 94.328,9.4444 94.328,8 94.328,4.504 91.496,1.67199 88,1.67199 c -1.4438,0 -2.7744,0.48303 -3.8389,1.29636 L 84.1597,2.96703 c -1.5132,1.15605 -2.489,2.97944 -2.489,5.03167 0,2.204 1.125,4.1425 2.8316,5.2743 0.5689,0.3773 1.2024,0.665 1.8804,0.843 V 12.9947 C 84.2744,12.3141 82.75,10.3359 82.75,8.0007 82.75,6.24712 83.6107,4.69363 84.9326,3.73989 85.7957,3.11707 86.8549,2.74933 88,2.74933 c 2.8987,0 5.2507,2.352 5.2507,5.25067 0,1.1457 -0.367,2.2055 -0.9898,3.0682 L 91.4175,10.2248 C 91.8348,9.585 92.0773,8.8206 92.0773,8 c 0,-2.16115 -1.6778,-3.92736 -3.803,-4.06827 -0.0904,-0.00598 -0.1817,-0.00839 -0.2736,-0.00839 -2.2534,0 -4.0774,1.82399 -4.0774,4.07736 0,1.6765 1.0124,3.1186 2.4594,3.7431 v -1.2171 c -0.832,-0.5334 -1.384,-1.4667 -1.384,-2.528 0,-1.65603 1.344,-3.00003 3,-3.00003 H 88 V 5 c 1.656,0 3,1.34399 3,3 0,0.5216 -0.1333,1.0122 -0.3676,1.4397 L 89.7447,8.552 C 89.7997,8.3778 89.8294,8.1924 89.8294,8 89.8294,6.98934 89.0107,6.17067 88,6.17067 c -0.1924,0 -0.3778,0.02966 -0.5519,0.08467 L 87.4467,6.254 c -0.7407,0.23396 -1.2774,0.92639 -1.2774,1.7447 0,0.824 0.544,1.52 1.2907,1.7493 v 5.728 C 87.092,15.452 86.7347,15.3987 86.3827,15.3214 85.9631,15.2293 85.5567,15.1021 85.1667,14.943 82.4288,13.8261 80.5,11.1382 80.5,7.9987 80.5,4.89266 82.3885,2.22766 85.0799,1.08929 85.977,0.70983 86.9633,0.5 87.9987,0.5 Z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 88.5387,11.1255 -0.7798,-0.7798 c -0.2597,-0.2597 -0.2597,-0.6807 0,-0.9404 0.2597,-0.2597 0.6807,-0.2597 0.9404,0 l 5.395,5.3949 c 0.2597,0.2597 0.2597,0.6808 0,0.9405 -0.2597,0.2597 -0.6808,0.2597 -0.9405,0 L 91.849,14.4359 c -0.6815,0.4082 -1.4333,0.7112 -2.233,0.8868 -0.352,0.0773 -0.7093,0.1306 -1.0773,0.1546 z m 1.0773,1.0774 v 1.9144 c 0.4831,-0.1268 0.9436,-0.3093 1.3742,-0.5402 l -0.804,-0.804 c -0.1845,0.0845 -0.3749,0.1586 -0.5702,0.2216 z" fill="context-fill" /> + <path d="m 88,8.63 c 0.3479,0 0.63,-0.2821 0.63,-0.63 0,-0.3479 -0.2821,-0.63 -0.63,-0.63 -0.3479,0 -0.63,0.2821 -0.63,0.63 0,0.3479 0.2821,0.63 0.63,0.63 z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 100.5,7.9987 c 0,-4.14137 3.357,-7.4987 7.499,-7.4987 2.061,0 3.927,0.83143 5.283,2.17715 1.369,1.35859 2.217,3.24172 2.217,5.32285 0,1.7671 -0.611,3.3911 -1.634,4.6725 l -0.834,-0.8337 c 0.413,-0.5403 0.741,-1.1493 0.964,-1.807 h -0.002 c 0.216,-0.6379 0.334,-1.3217 0.334,-2.0331 0,-1.75964 -0.718,-3.35103 -1.876,-4.49778 -1.143,-1.13084 -2.715,-1.82893 -4.451,-1.82893 -1.444,0 -2.774,0.48303 -3.839,1.29636 L 104.16,2.96702 c -1.513,1.15606 -2.489,2.97945 -2.489,5.03168 0,2.9387 2,5.4053 4.712,6.1173 v -1.1213 c -2.109,-0.6806 -3.633,-2.6588 -3.633,-4.994 0,-1.75358 0.861,-3.30707 2.183,-4.26081 0.863,-0.62282 1.922,-0.99056 3.067,-0.99056 2.899,0 5.251,2.352 5.251,5.25067 0,1.1457 -0.367,2.2055 -0.99,3.0682 l -0.844,-0.8434 c 0.418,-0.6398 0.66,-1.4042 0.66,-2.2248 0,-2.16115 -1.678,-3.92736 -3.803,-4.06827 -0.09,-0.00598 -0.181,-0.00839 -0.273,-0.00839 -2.254,0 -4.078,1.82399 -4.078,4.07736 0,1.6765 1.013,3.1186 2.46,3.7431 v -1.2171 c -0.832,-0.5334 -1.384,-1.4667 -1.384,-2.528 0,-1.65603 1.344,-3.00003 3,-3.00003 1.656,0 3,1.344 3,3.00003 0,1.0081 -0.498,1.9008 -1.261,2.4449 l 0.001,0.0013 c -0.04,0.0287 -0.081,0.0564 -0.123,0.0831 v 0.1942 c 0.206,-0.036 0.426,0.0257 0.585,0.1848 l 3.893,3.8932 c 0.26,0.2597 0.26,0.6808 0,0.9405 -0.259,0.2597 -0.68,0.2597 -0.94,0 l -1.305,-1.3048 c -0.682,0.4082 -1.433,0.7112 -2.233,0.8868 -0.352,0.0773 -0.709,0.1306 -1.077,0.1546 v -0.0014 l -0.002,1e-4 V 9.748 c 0.747,-0.2293 1.291,-0.9253 1.291,-1.7493 0,-1.01069 -0.819,-1.82936 -1.829,-1.82936 -1.011,0 -1.83,0.81867 -1.83,1.82936 0,0.824 0.544,1.52 1.291,1.7493 v 5.728 c -0.368,-0.024 -0.725,-0.0773 -1.077,-0.1546 -3.366,-0.7387 -5.883,-3.736 -5.883,-7.3227 z m 9.116,6.1169 v -1.9127 0.7918 c 0.195,-0.063 0.386,-0.1371 0.57,-0.2216 l 0.804,0.804 c -0.179,0.0963 -0.364,0.1842 -0.554,0.2631 v -0.0019 c -0.265,0.1104 -0.538,0.2033 -0.82,0.2773 z" fill="context-fill" /> + <path d="m 108,8.84 c 0.464,0 0.84,-0.3761 0.84,-0.84 0,-0.4639 -0.376,-0.84 -0.84,-0.84 -0.464,0 -0.84,0.3761 -0.84,0.84 0,0.4639 0.376,0.84 0.84,0.84 z" fill="context-fill" /> + <path d="m 127.999,0.5 c -4.142,0 -7.499,3.35733 -7.499,7.4987 0,3.5867 2.517,6.584 5.883,7.3227 0.352,0.0773 0.709,0.1306 1.077,0.1546 V 9.748 c -0.747,-0.2293 -1.291,-0.9253 -1.291,-1.7493 0,-1.01069 0.819,-1.82936 1.83,-1.82936 1.01,0 1.829,0.81867 1.829,1.82936 0,0.824 -0.544,1.52 -1.291,1.7493 v 5.728 l 0.002,-1e-4 v 0.0014 c 0.368,-0.024 0.725,-0.0773 1.077,-0.1546 0.8,-0.1756 1.551,-0.4786 2.233,-0.8868 l 1.305,1.3048 c 0.26,0.2597 0.681,0.2597 0.94,0 0.26,-0.2597 0.26,-0.6808 0,-0.9405 l -2.132,-2.1327 c -0.26,-0.2597 -0.681,-0.2597 -0.941,0 -0.249,0.2494 -0.259,0.6477 -0.03,0.9089 -0.18,0.0966 -0.365,0.1847 -0.555,0.2638 v -0.0019 c -0.265,0.1104 -0.538,0.2033 -0.82,0.2773 V 10.528 c 0.042,-0.0267 0.083,-0.0544 0.123,-0.0831 l -0.001,-0.0013 c 0.763,-0.5441 1.261,-1.4368 1.261,-2.4449 0,-1.65603 -1.344,-3.00003 -3,-3.00003 -1.656,0 -3,1.344 -3,3.00003 0,1.0613 0.552,1.9946 1.384,2.528 v 1.2171 c -1.447,-0.6245 -2.46,-2.0666 -2.46,-3.7431 0,-2.25337 1.824,-4.07736 4.078,-4.07736 2.253,0 4.077,1.82399 4.077,4.07736 0,1.3054 -0.614,2.4688 -1.569,3.2146 -0.272,0.2125 -0.572,0.3904 -0.893,0.5287 v 1.2507 c 0,-10e-5 0,10e-5 0,0 2.109,-0.68 3.635,-2.658 3.635,-4.994 0,-2.8987 -2.352,-5.2507 -5.25,-5.2507 -2.899,0 -5.251,2.35201 -5.251,5.2507 0,2.3352 1.524,4.3134 3.633,4.994 v 1.1213 c -2.712,-0.712 -4.712,-3.1786 -4.712,-6.1173 0,-2.05223 0.976,-3.87562 2.489,-5.03168 l 0.001,0.00133 c 1.065,-0.81333 2.395,-1.29636 3.839,-1.29636 1.736,0 3.308,0.69809 4.451,1.82893 1.158,1.14675 1.876,2.73814 1.876,4.49778 0,0.7114 -0.118,1.3952 -0.334,2.0331 h 0.002 c -0.223,0.6577 -0.551,1.2667 -0.964,1.807 l 0.834,0.8337 c 1.023,-1.2814 1.634,-2.9054 1.634,-4.6725 0,-2.08113 -0.848,-3.96426 -2.217,-5.32285 C 131.926,1.33143 130.06,0.5 127.999,0.5 Z" fill="context-fill" /> + <path d="m 128,8.84 c 0.464,0 0.84,-0.3761 0.84,-0.84 0,-0.4639 -0.376,-0.84 -0.84,-0.84 -0.464,0 -0.84,0.3761 -0.84,0.84 0,0.4639 0.376,0.84 0.84,0.84 z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 152.493,14.139 c 0.259,-0.2597 0.68,-0.2597 0.94,0 l 0.661,0.6612 c 0.26,0.2597 0.26,0.6808 0,0.9405 -0.259,0.2597 -0.68,0.2597 -0.94,0 l -0.661,-0.6612 c -0.26,-0.2597 -0.26,-0.6808 0,-0.9405 z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 140.5,7.9987 c 0,-4.14137 3.357,-7.4987 7.499,-7.4987 4.141,0 7.498,3.35733 7.498,7.4987 0,3.5867 -2.517,6.584 -5.882,7.3227 -0.352,0.0773 -0.71,0.1306 -1.078,0.1546 V 9.748 c 0.747,-0.2293 1.291,-0.9253 1.291,-1.7493 0,-1.01069 -0.819,-1.82936 -1.829,-1.82936 -1.011,0 -1.83,0.81867 -1.83,1.82936 0,0.824 0.544,1.52 1.291,1.7493 v 5.728 c -0.368,-0.024 -0.725,-0.0773 -1.077,-0.1546 -3.366,-0.7387 -5.883,-3.736 -5.883,-7.3227 z m 9.115,2.528 v 3.5893 c 2.712,-0.712 4.712,-3.1786 4.712,-6.1173 0,-3.49602 -2.832,-6.32802 -6.328,-6.32802 -3.496,0 -6.328,2.83199 -6.328,6.32802 0,2.9387 2,5.4053 4.712,6.1173 v -1.1213 c -2.109,-0.6806 -3.633,-2.6588 -3.633,-4.994 0,-2.89869 2.352,-5.2507 5.251,-5.2507 2.898,0 5.25,2.352 5.25,5.2507 0,2.336 -1.525,4.3147 -3.634,4.9947 v -1.2507 c 1.448,-0.624 2.461,-2.0667 2.461,-3.744 0,-2.25337 -1.824,-4.07736 -4.077,-4.07736 -2.254,0 -4.078,1.82399 -4.078,4.07736 0,1.6765 1.013,3.1186 2.46,3.7431 v -1.2171 c -0.832,-0.5334 -1.384,-1.4667 -1.384,-2.528 0,-1.65603 1.344,-3.00003 3,-3.00003 1.656,0 3,1.344 3,3.00003 0,1.0613 -0.552,1.9946 -1.384,2.528 z" fill="context-fill" /> + <path d="m 148,8.84 c 0.464,0 0.84,-0.3761 0.84,-0.84 0,-0.4639 -0.376,-0.84 -0.84,-0.84 -0.464,0 -0.84,0.3761 -0.84,0.84 0,0.4639 0.376,0.84 0.84,0.84 z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 168.001,2.75 c -2.899,0 -5.251,2.35201 -5.251,5.2507 0,2.336 1.525,4.3147 3.635,4.9947 v -1.2507 c -1.448,-0.624 -2.462,-2.0667 -2.462,-3.744 0,-2.25337 1.824,-4.07736 4.078,-4.07736 2.253,0 4.077,1.82399 4.077,4.07736 0,1.6773 -1.013,3.12 -2.461,3.744 v 1.2507 c 2.109,-0.68 3.634,-2.6587 3.634,-4.9947 0,-2.8987 -2.352,-5.2507 -5.25,-5.2507 z" fill="context-fill" /> + <path fill-rule="evenodd" d="m 167.999,0.5 c -4.142,0 -7.499,3.35733 -7.499,7.4987 0,3.5867 2.517,6.584 5.883,7.3227 0.352,0.0773 0.709,0.1306 1.077,0.1546 V 9.748 c -0.747,-0.2293 -1.291,-0.9253 -1.291,-1.7493 0,-1.01069 0.819,-1.82936 1.83,-1.82936 1.01,0 1.829,0.81867 1.829,1.82936 0,0.824 -0.544,1.52 -1.291,1.7493 v 5.728 c 0.368,-0.024 0.726,-0.0773 1.078,-0.1546 3.365,-0.7387 5.882,-3.736 5.882,-7.3227 0,-4.14137 -3.357,-7.4987 -7.498,-7.4987 z m 1.616,13.616 v -3.5893 c 0.832,-0.5334 1.384,-1.4667 1.384,-2.528 0,-1.65603 -1.344,-3.00003 -3,-3.00003 -1.656,0 -3,1.344 -3,3.00003 0,1.0613 0.552,1.9946 1.384,2.528 v 3.5893 c -2.712,-0.712 -4.712,-3.1786 -4.712,-6.1173 0,-3.49603 2.832,-6.32802 6.328,-6.32802 3.496,0 6.328,2.832 6.328,6.32802 0,2.9387 -2,5.4053 -4.712,6.1173 z" fill="context-fill" /> + <path d="m 168,8.84 c 0.464,0 0.84,-0.3761 0.84,-0.84 0,-0.4639 -0.376,-0.84 -0.84,-0.84 -0.464,0 -0.84,0.3761 -0.84,0.84 0,0.4639 0.376,0.84 0.84,0.84 z" fill="context-fill" /> +</svg> diff --git a/toolkit/components/torconnect/content/torConnectTitlebarStatus.css b/toolkit/components/torconnect/content/torConnectTitlebarStatus.css @@ -0,0 +1,90 @@ +.tor-connect-titlebar-status:not([hidden]) { + display: flex; + align-items: center; + /* Want same as .private-browsing-indicator-with-label */ + margin-inline: 7px; + + #navigator-toolbox[tabs-hidden] #TabsToolbar > & { + /* Hide in the tabs bar when the tabs bar is hidden. E.g. when using + * vertical tabs. Should be shown in the #nav-bar instead. + * See tor-browser#44101. */ + display: none; + } + + #navigator-toolbox:not([tabs-hidden]) #nav-bar > & { + /* Hide in the nav bar when the (horizontal) tabs bar is visible. */ + display: none; + } +} + +.tor-connect-titlebar-status-label { + margin-inline: 6px; + white-space: nowrap; +} + +.tor-connect-titlebar-status img { + -moz-context-properties: fill, stroke; + fill: var(--icon-color); + stroke: var(--icon-color); + width: 16px; + height: 16px; + object-fit: none; + --num-animation-steps: 8; + /* First frame has no offset. */ + --tor-not-connected-offset: 0; + /* Each frame/step is offset by 20px from the previous. */ + --tor-connected-offset: calc(-20px * var(--num-animation-steps)); + object-position: var(--tor-not-connected-offset); +} + +.tor-connect-titlebar-status.tor-connect-status-potentially-blocked img { + /* NOTE: context-stroke is only used for the first "frame" for the slash. When + * we assign the potentially-blocked class, we do *not* expect to be connected + * at the same time, so we only expect this first frame to be visible in this + * state. */ + stroke: var(--icon-color-critical); +} + +.tor-connect-titlebar-status.tor-connect-status-connected img { + object-position: var(--tor-connected-offset); +} + +@media not ((prefers-contrast) or (forced-colors)) { + /* Make the connected text and icon purple. */ + .tor-connect-titlebar-status.tor-connect-status-connected { + color: var(--tor-text-color); + } + + .tor-connect-titlebar-status.tor-connect-status-connected img { + fill: var(--tor-text-color); + stroke: var(--tor-text-color); + } +} + +@keyframes onion-not-connected-to-connected { + from { + object-position: var(--tor-not-connected-offset); + } + + to { + object-position: var(--tor-connected-offset); + } +} + +@media (prefers-reduced-motion: no-preference) { + .tor-connect-titlebar-status.tor-connect-status-connected.tor-connect-status-animate-transition { + transition: color 1000ms; + } + + .tor-connect-titlebar-status.tor-connect-status-connected.tor-connect-status-animate-transition img { + transition: + fill 1000ms, + stroke 1000ms; + animation-name: onion-not-connected-to-connected; + animation-delay: 200ms; + animation-fill-mode: both; + /* Run animation at 60 frames-per-second. */ + animation-duration: calc(var(--num-animation-steps) * 1000ms / 60); + animation-timing-function: steps(var(--num-animation-steps)); + } +} diff --git a/toolkit/components/torconnect/content/torConnectTitlebarStatus.inc.xhtml b/toolkit/components/torconnect/content/torConnectTitlebarStatus.inc.xhtml @@ -0,0 +1,7 @@ +<html:div class="tor-connect-titlebar-status" role="status"> + <html:img + alt="" + src="chrome://global/content/torconnect/tor-not-connected-to-connected-animated.svg" + /> + <html:span class="tor-connect-titlebar-status-label"></html:span> +</html:div> diff --git a/toolkit/components/torconnect/content/torConnectTitlebarStatus.js b/toolkit/components/torconnect/content/torConnectTitlebarStatus.js @@ -0,0 +1,170 @@ +/** + * A TorConnect status shown in the application title bar. + */ +var gTorConnectTitlebarStatus = { + /** + * The status elements and their labels. + * + * @type {{status: Element, label: Element}[]} + */ + _elements: [], + /** + * Whether we are connected, or null if the connection state is not yet known. + * + * @type {boolean?} + */ + connected: null, + + /** + * Initialize the component. + */ + init() { + const { TorStrings } = ChromeUtils.importESModule( + "resource://gre/modules/TorStrings.sys.mjs" + ); + + this._strings = TorStrings.torConnect; + + this._elements = Array.from( + document.querySelectorAll(".tor-connect-titlebar-status"), + element => { + return { + status: element, + label: element.querySelector(".tor-connect-titlebar-status-label"), + }; + } + ); + // The title also acts as an accessible name for the role="status". + for (const { status } of this._elements) { + status.setAttribute("title", this._strings.titlebarStatusName); + } + + Services.obs.addObserver(this, TorConnectTopics.StageChange); + + this._torConnectStateChanged(); + }, + + /** + * De-initialize the component. + */ + uninit() { + Services.obs.removeObserver(this, TorConnectTopics.StageChange); + }, + + observe(subject, topic) { + switch (topic) { + case TorConnectTopics.StageChange: + this._torConnectStateChanged(); + break; + } + }, + + /** + * Callback for when the TorConnect state changes. + */ + _torConnectStateChanged() { + let textId; + let connected = false; + let potentiallyBlocked = false; + switch (TorConnect.stageName) { + case TorConnectStage.Disabled: + // Hide immediately. + this._setHidden(true); + return; + case TorConnectStage.Bootstrapped: + textId = "titlebarStatusConnected"; + connected = true; + break; + case TorConnectStage.Bootstrapping: + textId = "titlebarStatusConnecting"; + break; + default: + if (TorConnect.potentiallyBlocked) { + textId = "titlebarStatusPotentiallyBlocked"; + potentiallyBlocked = true; + } else { + textId = "titlebarStatusNotConnected"; + } + break; + } + for (const { label } of this._elements) { + label.textContent = this._strings[textId]; + } + if (this.connected !== connected) { + // When we are transitioning from + // this.connected = false + // to + // this.connected = true + // we want to animate the transition from the not connected state to the + // connected state (provided prefers-reduced-motion is not set). + // + // If instead we are transitioning directly from the initial state + // this.connected = null + // to + // this.connected = true + // we want to immediately show the connected state without any transition. + // + // In both cases, the status will eventually be hidden. + // + // We only expect this latter case when opening a new window after + // bootstrapping has already completed. See tor-browser#41850. + for (const { status } of this._elements) { + status.classList.toggle( + "tor-connect-status-animate-transition", + connected && this.connected !== null + ); + status.classList.toggle("tor-connect-status-connected", connected); + } + this.connected = connected; + if (connected) { + this._startHiding(); + } else { + // We can leave the connected state when we are no longer Bootstrapped + // because the underlying tor process exited early and needs a + // restart. In this case we want to re-show the status. + this._stopHiding(); + } + } + for (const { status } of this._elements) { + status.classList.toggle( + "tor-connect-status-potentially-blocked", + potentiallyBlocked + ); + } + }, + + /** + * Hide or show the status. + * + * @param {boolean} hide - Whether to hide the status. + */ + _setHidden(hide) { + for (const { status } of this._elements) { + status.hidden = hide; + } + }, + + /** + * Mark the component to be hidden after some delay. + */ + _startHiding() { + if (this._hidingTimeout) { + // Already hiding. + return; + } + this._hidingTimeout = setTimeout(() => { + this._setHidden(true); + }, 5000); + }, + + /** + * Re-show the component immediately. + */ + _stopHiding() { + if (this._hidingTimeout) { + clearTimeout(this._hidingTimeout); + this._hidingTimeout = 0; + } + this._setHidden(false); + }, +}; diff --git a/toolkit/components/torconnect/content/torConnectUrlbarButton.js b/toolkit/components/torconnect/content/torConnectUrlbarButton.js @@ -0,0 +1,147 @@ +/** + * A "Connect" button shown in the urlbar when not connected to tor and in tabs + * other than about:torconnect. + */ +var gTorConnectUrlbarButton = { + /** + * The urlbar button node. + * + * @type {Element} + */ + button: null, + /** + * Whether we are active. + * + * @type {boolean} + */ + _isActive: false, + /** + * Whether we are in the "about:torconnect" tab. + * + * @type {boolean} + */ + // We init to "true" so that the button can only appear after the first page + // load. + _inAboutTorConnectTab: true, + + /** + * Initialize the button. + */ + init() { + if (this._isActive) { + return; + } + + this.button = document.getElementById("tor-connect-urlbar-button"); + + if (!TorConnect.enabled) { + // Don't initialise, just hide. + this._updateButtonVisibility(); + return; + } + + this._isActive = true; + + const { TorStrings } = ChromeUtils.importESModule( + "resource://gre/modules/TorStrings.sys.mjs" + ); + + document.getElementById("tor-connect-urlbar-button-label").value = + TorStrings.torConnect.torConnectButton; + this.button.addEventListener("click", event => { + if (event.button !== 0) { + return; + } + this.connect(); + }); + this.button.addEventListener("keydown", event => { + if (event.key !== "Enter" && event.key !== " ") { + return; + } + this.connect(); + }); + + this._observeTopic = TorConnectTopics.StageChange; + this._stateListener = { + observe: (subject, topic) => { + if (topic !== this._observeTopic) { + return; + } + this._updateButtonVisibility(); + }, + }; + Services.obs.addObserver(this._stateListener, this._observeTopic); + + this._locationListener = { + onLocationChange: (webProgress, request, locationURI, flags) => { + if ( + webProgress.isTopLevel && + !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) + ) { + this._inAboutTorConnectTab = + gBrowser.selectedBrowser.currentURI?.spec.startsWith( + "about:torconnect" + ); + this._updateButtonVisibility(); + } + }, + }; + // Notified of new locations for the currently selected browser (tab) *and* + // switching selected browser. + gBrowser.addProgressListener(this._locationListener); + + this._updateButtonVisibility(); + }, + + /** + * Deactivate and de-initialize the button. + */ + uninit() { + if (!this._isActive) { + return; + } + this._isActive = false; + + Services.obs.removeObserver(this._stateListener, this._observeTopic); + gBrowser.removeProgressListener(this._locationListener); + this._updateButtonVisibility(); + }, + + /** + * Begin the tor connection bootstrapping process. + */ + connect() { + TorConnectParent.open({ beginBootstrapping: "soft" }); + }, + + /** + * Callback when the TorConnect state, current browser location, or activation + * state changes. + */ + _updateButtonVisibility() { + if (!this.button) { + return; + } + const hadFocus = this.button.contains(document.activeElement); + const hide = + !this._isActive || + this._inAboutTorConnectTab || + TorConnect.stageName === TorConnectStage.Bootstrapped; + this.button.hidden = hide; + if (hide && hadFocus) { + // Lost focus. E.g. if the "Connect" button is focused in another window + // or tab outside of about:torconnect. + // Move focus back to the URL bar. + gURLBar.focus(); + } + // We style the button as a tor purple button if clicking the button will + // also start a bootstrap. I.e. whether we meet the conditions in + // TorConnectParent.open. + const plainButton = + !this._isActive || + !TorConnect.canBeginNormalBootstrap || + TorConnect.potentiallyBlocked; + this.button.classList.toggle("tor-urlbar-button-plain", plainButton); + this.button.classList.toggle("tor-button", !plainButton); + }, +}; diff --git a/toolkit/components/torconnect/jar.mn b/toolkit/components/torconnect/jar.mn @@ -0,0 +1,14 @@ +toolkit.jar: + content/global/torconnect/torConnectUrlbarButton.js (content/torConnectUrlbarButton.js) + content/global/torconnect/torConnectTitlebarStatus.js (content/torConnectTitlebarStatus.js) + content/global/torconnect/torConnectTitlebarStatus.css (content/torConnectTitlebarStatus.css) + content/global/torconnect/aboutTorConnect.css (content/aboutTorConnect.css) + content/global/torconnect/aboutTorConnect.html (content/aboutTorConnect.html) + content/global/torconnect/aboutTorConnect.js (content/aboutTorConnect.js) + content/global/torconnect/arrow-right.svg (content/arrow-right.svg) + content/global/torconnect/bridge.svg (content/bridge.svg) + content/global/torconnect/connection-failure.svg (content/connection-failure.svg) + content/global/torconnect/connection-location.svg (content/connection-location.svg) + content/global/torconnect/tor-connect.svg (content/tor-connect.svg) + content/global/torconnect/tor-not-connected-to-connected-animated.svg (content/tor-not-connected-to-connected-animated.svg) + content/global/torconnect/tor-connect-broken.svg (content/tor-connect-broken.svg) diff --git a/toolkit/components/torconnect/moz.build b/toolkit/components/torconnect/moz.build @@ -0,0 +1,6 @@ +JAR_MANIFESTS += ["jar.mn"] + +FINAL_TARGET_FILES.actors += [ + "TorConnectChild.sys.mjs", + "TorConnectParent.sys.mjs", +] diff --git a/toolkit/content/.eslintrc.mjs b/toolkit/content/.eslintrc.mjs @@ -9,7 +9,7 @@ export default [ rules: { // XXX Bug 1358949 - This should be reduced down - probably to 20 or to // be removed & synced with the mozilla/recommended value. - complexity: ["error", 48], + complexity: ["error", 49], }, }, { diff --git a/toolkit/content/aboutNetError.mjs b/toolkit/content/aboutNetError.mjs @@ -291,7 +291,7 @@ function setResponseStatus(shortDesc) { } // Returns pageTitleId, bodyTitle, bodyTitleId, and longDesc as an object -function initTitleAndBodyIds(baseURL, isTRROnlyFailure) { +async function initTitleAndBodyIds(baseURL, isTRROnlyFailure) { let bodyTitle = document.querySelector(".title-text"); let longDesc = document.getElementById("errorLongDesc"); const tryAgain = document.getElementById("netErrorButtonContainer"); @@ -387,12 +387,22 @@ function initTitleAndBodyIds(baseURL, isTRROnlyFailure) { learnMore.hidden = false; document.body.className = "certerror"; break; + + case "proxyConnectFailure": + if (await RPMSendQuery("ShouldShowTorConnect")) { + // pass orginal destination as redirect param + const encodedRedirect = encodeURIComponent(document.location.href); + document.location.replace( + `about:torconnect?redirect=${encodedRedirect}` + ); + } + break; } return { pageTitleId, bodyTitle, bodyTitleId, longDesc }; } -function initPage() { +async function initPage() { // We show an offline support page in case of a system-wide error, // when a user cannot connect to the internet and access the SUMO website. // For example, clock error, which causes certerrors across the web or @@ -459,10 +469,8 @@ function initPage() { tryAgain.hidden = false; const learnMoreLink = document.getElementById("learnMoreLink"); learnMoreLink.setAttribute("href", baseURL + "connection-not-secure"); - let { pageTitleId, bodyTitle, bodyTitleId, longDesc } = initTitleAndBodyIds( - baseURL, - isTRROnlyFailure - ); + let { pageTitleId, bodyTitle, bodyTitleId, longDesc } = + await initTitleAndBodyIds(baseURL, isTRROnlyFailure); // We can handle the offline page separately. if (gNoConnectivity) { diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs @@ -545,6 +545,20 @@ let JSWINDOWACTORS = { allFrames: true, }, + TorConnect: { + parent: { + esModuleURI: "resource://gre/actors/TorConnectParent.sys.mjs", + }, + child: { + esModuleURI: "resource://gre/actors/TorConnectChild.sys.mjs", + events: { + DOMWindowCreated: {}, + }, + }, + + matches: ["about:torconnect", "about:torconnect?*"], + }, + // This actor is available for all pages that one can // view the source of, however it won't be created until a // request to view the source is made via the message diff --git a/toolkit/modules/RemotePageAccessManager.sys.mjs b/toolkit/modules/RemotePageAccessManager.sys.mjs @@ -71,6 +71,7 @@ export let RemotePageAccessManager = { RPMAddMessageListener: ["WWWReachable"], RPMTryPingSecureWWWLink: ["*"], RPMOpenSecureWWWLink: ["*"], + RPMSendQuery: ["ShouldShowTorConnect"], }, "about:certificate": { RPMSendQuery: ["getCertificates"], @@ -124,7 +125,7 @@ export let RemotePageAccessManager = { RPMIsSiteSpecificTRRError: ["*"], RPMSetTRRDisabledLoadFlags: ["*"], RPMShowOSXLocalNetworkPermissionWarning: ["*"], - RPMSendQuery: ["Browser:AddTRRExcludedDomain"], + RPMSendQuery: ["Browser:AddTRRExcludedDomain", "ShouldShowTorConnect"], RPMGetIntPref: ["network.trr.mode"], }, "about:newtab": { @@ -263,6 +264,25 @@ export let RemotePageAccessManager = { RPMAddMessageListener: ["*"], RPMRemoveMessageListener: ["*"], }, + "about:torconnect": { + RPMAddMessageListener: [ + "torconnect:stage-change", + "torconnect:bootstrap-progress", + "torconnect:quickstart-change", + "torconnect:region-names-change", + ], + RPMSendAsyncMessage: [ + "torconnect:open-tor-preferences", + "torconnect:begin-bootstrapping", + "torconnect:cancel-bootstrapping", + "torconnect:set-quickstart", + "torconnect:view-tor-logs", + "torconnect:restart", + "torconnect:start-again", + "torconnect:choose-region", + ], + RPMSendQuery: ["torconnect:get-init-args", "torconnect:get-regions"], + }, "about:welcome": { RPMSendAsyncMessage: ["ActivityStream:ContentToMain"], RPMAddMessageListener: ["ActivityStream:MainToContent"],