commit c066854822a1004258ae85aa2d9a5a65cb660592 parent 62d880e6871af6b44989c657d13a23ae0b0402ca Author: Henry Wilkes <henry@torproject.org> Date: Wed, 2 Aug 2023 12:18:08 +0100 TB 7494: Create local home page for TBB. Bug 41333: Update about:tor to new design. Including: + make the favicon match the branding icon. + make the location bar show a search icon. Diffstat:
28 files changed, 1700 insertions(+), 2 deletions(-)
diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js @@ -1445,6 +1445,10 @@ var BookmarkingUI = { newTabURL, "about:home", "chrome://browser/content/blanktab.html", + // Add the "about:tor" uri. See tor-browser#41717. + // NOTE: "about:newtab", "about:welcome", "about:home" and + // "about:privatebrowsing" can also redirect to "about:tor". + "about:tor", ]; if (PrivateBrowsingUtils.isWindowPrivate(window)) { newTabURLs.push("about:privatebrowsing"); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -744,6 +744,7 @@ async function gLazyFindCommand(cmd, ...args) { } var gPageIcons = { + "about:tor": "chrome://branding/content/icon32.png", "about:home": "chrome://branding/content/icon32.png", "about:newtab": "chrome://branding/content/icon32.png", "about:opentabs": "chrome://branding/content/icon32.png", @@ -752,6 +753,7 @@ var gPageIcons = { }; var gInitialPages = [ + "about:tor", "about:torconnect", "about:blank", "about:home", diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js @@ -83,6 +83,7 @@ function isBlankPageURL(aURL) { return ( aURL == "about:blank" || aURL == "about:home" || + aURL == "about:tor" || aURL == BROWSER_NEW_TAB_URL || aURL == "chrome://browser/content/blanktab.html" ); diff --git a/browser/components/BrowserComponents.manifest b/browser/components/BrowserComponents.manifest @@ -18,6 +18,7 @@ category browser-before-ui-startup resource:///modules/BuiltInThemes.sys.mjs Bui category browser-before-ui-startup resource://normandy/Normandy.sys.mjs Normandy.init #endif category browser-before-ui-startup moz-src:///browser/components/privatebrowsing/ResetPBMPanel.sys.mjs ResetPBMPanel.init +category browser-before-ui-startup resource:///modules/HomepageOverride.sys.mjs HomepageOverride.check # newtab component is disabled. tor-browser#43886 category browser-before-ui-startup resource:///modules/AccountsGlue.sys.mjs AccountsGlue.init category browser-before-ui-startup moz-src:///browser/modules/ObserverForwarder.sys.mjs ObserverForwarder.init diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs @@ -779,6 +779,7 @@ nsBrowserContentHandler.prototype = { var overridePage = ""; var additionalPage = ""; var willRestoreSession = false; + let openAboutTor = false; try { // Read the old value of homepage_override.mstone before // needHomepageOverride updates it, so that we can later add it to the @@ -977,6 +978,19 @@ nsBrowserContentHandler.prototype = { "%OLD_BASE_BROWSER_VERSION%", old_forkVersion ); + if (AppConstants.BASE_BROWSER_UPDATE) { + // Tor Browser: Instead of opening the post-update "override page" + // directly, we ensure that about:tor will be opened, which should + // notify the user that their browser was updated. + // NOTE: We ignore any existing overridePage value, which can come + // from the openURL attribute within the updates.xml file. + Services.prefs.setBoolPref( + "torbrowser.post_update.shouldNotify", + true + ); + openAboutTor = true; + overridePage = "about:tor"; + } break; } case OVERRIDE_NEW_BUILD_ID: { @@ -1076,6 +1090,16 @@ nsBrowserContentHandler.prototype = { startPage = ""; } + // If the user's homepage is about:tor, we do not want to open it twice with + // the override. + if ( + openAboutTor && + startPage === "about:tor" && + overridePage?.split("|").includes("about:tor") + ) { + startPage = ""; + } + // Only show the startPage if we're not restoring an update session and are // not set to skip the start page on this profile if (overridePage && startPage && !willRestoreSession && !skipStartPage) { diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs @@ -198,6 +198,25 @@ let JSWINDOWACTORS = { matches: ["about:tabcrashed*"], }, + AboutTor: { + parent: { + esModuleURI: "resource:///actors/AboutTorParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/AboutTorChild.sys.mjs", + + events: { + DOMContentLoaded: {}, + L10nMutationsFinished: {}, + SubmitSearchOnionize: { wantUntrusted: true }, + SurveyDismissed: { wantUntrusted: true }, + UserDismissedYEC: { wantUntrusted: true }, + }, + }, + + matches: ["about:tor"], + }, + AboutWelcome: { parent: { esModuleURI: "resource:///actors/AboutWelcomeParent.sys.mjs", diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp @@ -21,7 +21,18 @@ #define ABOUT_WELCOME_CHROME_URL \ "chrome://browser/content/aboutwelcome/aboutwelcome.html" #define ABOUT_HOME_URL "about:home" -#define BASE_BROWSER_HOME_PAGE_URL "chrome://browser/content/blanktab.html" +// NOTE: We return "about:tor" rather than the "chrome:" path +// "chrome://browser/content/abouttor/aboutTor.html" +// The result is that the channel created in NewChannel in will have its +// resultPrincipalURI set to "about:tor". +// What this means in practice is that the loaded document's documentURI and +// currentURI will be "about:tor" rather than "about:newtab", "about:home", +// "about:welcome" or "about:privatebrowsing". +// The disadvantage of this is that we often need to add "about:tor" to places +// where "about:newtab" or other URIs appear. +// The advantage is that we maintain more control against changes in +// mozilla-central. +#define BASE_BROWSER_HOME_PAGE_URL "about:tor" namespace mozilla { namespace browser { @@ -161,6 +172,8 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::IS_SECURE_CHROME_UI | nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::URI_MUST_LOAD_IN_CHILD | nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS}, + {"tor", "chrome://browser/content/abouttor/aboutTor.html", + BASE_BROWSER_HOME_PAGE_FLAGS}, }; static nsAutoCString GetAboutModuleName(nsIURI* aURI) { diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf @@ -29,6 +29,7 @@ pages = [ 'sessionrestore', 'settings', 'tabcrashed', + 'tor', 'unloads', 'welcome', 'welcomeback', diff --git a/browser/components/abouttor/AboutTorChild.sys.mjs b/browser/components/abouttor/AboutTorChild.sys.mjs @@ -0,0 +1,61 @@ +/** + * Actor child class for the about:tor page. + */ +export class AboutTorChild extends JSWindowActorChild { + handleEvent(event) { + switch (event.type) { + case "DOMContentLoaded": + this.sendQuery("AboutTor:GetInitialData").then(data => { + if (data) { + this.#dispatchInitialData(data); + } + }); + break; + case "SubmitSearchOnionize": + this.sendAsyncMessage("AboutTor:SetSearchOnionize", !!event.detail); + break; + case "SurveyDismissed": + // event.detail is the survey version. + this.sendAsyncMessage("AboutTor:SurveyDismissed", event.detail); + break; + case "L10nMutationsFinished": + // Pass on chrome-only event for completed localization to content. + this.contentWindow.dispatchEvent( + new this.contentWindow.CustomEvent("L10nMutationsFinished") + ); + break; + case "UserDismissedYEC": + // YEC banner was closed. Persist this for the rest of this session. + // See tor-browser#42188. + this.sendAsyncMessage("AboutTor:UserDismissedYEC"); + break; + } + } + + receiveMessage(message) { + switch (message.name) { + case "AboutTor:DelayedInitialData": + this.#dispatchInitialData(message.data); + break; + case "AboutTor:DismissYEC": { + this.contentWindow.dispatchEvent( + new this.contentWindow.CustomEvent("DismissYEC") + ); + break; + } + } + return undefined; + } + + /** + * Send the initial data to the page. + * + * @param {object} data - The data to send. + */ + #dispatchInitialData(data) { + const initialDataEvent = new this.contentWindow.CustomEvent("InitialData", { + detail: Cu.cloneInto(data, this.contentWindow), + }); + this.contentWindow.dispatchEvent(initialDataEvent); + } +} diff --git a/browser/components/abouttor/AboutTorMessage.sys.mjs b/browser/components/abouttor/AboutTorMessage.sys.mjs @@ -0,0 +1,50 @@ +// about:tor should cycle its displayed message on each load, this keeps track +// of which message to show globally. + +/** + * @typedef {object} MessageData + * + * @property {string} [updateVersion] - The update version to show. If this is + * defined, the update message should be shown. + * @property {string} [updateURL] - The update URL to use when updateVersion is + * given. + * @property {integer} [number] - The number of the message to show, when + * updateVersion is not given. This always increases, so the caller should + * take its remainder to cycle messages. + */ +export const AboutTorMessage = { + // NOTE: We always start the count at 0 with every session so that the first + // message is always shown first. + _count: 0, + + /** + * Get details about which message to show on the next about:tor page. + * + * @returns {MessageData} Details about the message to show. + */ + getNext() { + const shouldNotifyPref = "torbrowser.post_update.shouldNotify"; + if (Services.prefs.getBoolPref(shouldNotifyPref, false)) { + Services.prefs.clearUserPref(shouldNotifyPref); + // Try use the same URL as the about dialog. See tor-browser#43567. + let updateURL = Services.urlFormatter.formatURLPref( + "app.releaseNotesURL.aboutDialog" + ); + if (updateURL === "about:blank") { + updateURL = Services.urlFormatter.formatURLPref( + "startup.homepage_override_url" + ); + } + return { + updateVersion: Services.prefs.getCharPref( + "browser.startup.homepage_override.torbrowser.version" + ), + updateURL, + }; + } + const number = this._count; + // Assume the count will not exceed Number.MAX_SAFE_INTEGER. + this._count++; + return { number }; + }, +}; diff --git a/browser/components/abouttor/AboutTorParent.sys.mjs b/browser/components/abouttor/AboutTorParent.sys.mjs @@ -0,0 +1,132 @@ +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AboutTorMessage: "resource:///modules/AboutTorMessage.sys.mjs", + TorConnect: "resource://gre/modules/TorConnect.sys.mjs", +}); + +const initializedActors = new Set(); +const onionizePref = "torbrowser.homepage.search.onionize"; +const surveyDismissVersionPref = "torbrowser.homepage.survey.dismiss_version"; + +/** + * Actor parent class for the about:tor page. + */ +export class AboutTorParent extends JSWindowActorParent { + /** + * Whether the user has dismissed the Year End Campaign (YEC) banner this + * session. + * + * @type {boolean} + */ + static #dismissYEC = false; + + /** + * Whether this instance has a preloaded browser. + * + * @type {boolean} + */ + #preloaded = false; + + /** + * Method to be called when the browser corresponding to this actor has its + * preloadedState attribute removed. + */ + preloadedRemoved() { + if (!this.#preloaded) { + return; + } + this.#preloaded = false; + // Send in the initial data now that the page is actually going to be + // visible. + this.sendAsyncMessage( + "AboutTor:DelayedInitialData", + this.#getInitialData() + ); + } + + /** + * Get the initial data for the page when it is about to be shown. + * + * @returns {object} - The initial data. + */ + #getInitialData() { + let appLocale = Services.locale.appLocaleAsBCP47; + if (appLocale === "ja-JP-macos") { + appLocale = "ja"; + } + + return { + torConnectEnabled: lazy.TorConnect.enabled, + messageData: lazy.AboutTorMessage.getNext(), + isStable: AppConstants.MOZ_UPDATE_CHANNEL === "release", + searchOnionize: Services.prefs.getBoolPref(onionizePref, false), + surveyDismissVersion: Services.prefs.getIntPref( + surveyDismissVersionPref, + 0 + ), + appLocale, + dismissYEC: AboutTorParent.#dismissYEC, + }; + } + + didDestroy() { + initializedActors.delete(this); + } + + receiveMessage(message) { + switch (message.name) { + case "AboutTor:GetInitialData": { + // Track this actor to send future updates. + initializedActors.add(this); + + const browser = this.browsingContext.top.embedderElement; + if (browser?.getAttribute("preloadedState") === "preloaded") { + // Wait until the page is actually about to be shown before sending + // the initial data. + // Otherwise the preloaded page might receive data that has expired by + // the time the page is shown. And it will iterate + // AboutTorMessage.getNext too early. See tor-browser#44314. + this.#preloaded = true; + return Promise.resolve(null); + } + return Promise.resolve(this.#getInitialData()); + } + case "AboutTor:SetSearchOnionize": + Services.prefs.setBoolPref(onionizePref, message.data); + break; + case "AboutTor:SurveyDismissed": + // The message.data contains the version of the current survey. + // Rather than introduce a new preference for each survey campaign we + // reuse the same integer preference and increase its value every time + // a new version of the survey is shown and dismissed by the user. + // I.e. if the preference value is 2, we will not show survey version 2 + // but will show survey version 3 or higher when they are introduced. + // It should be safe to overwrite the value since we do not expect more + // than one active survey campaign at any given time, nor do we expect + // the version value to decrease. + Services.prefs.setIntPref(surveyDismissVersionPref, message.data); + break; + case "AboutTor:UserDismissedYEC": + AboutTorParent.#dismissYEC = true; + for (const actor of initializedActors) { + if (actor === this) { + // Don't send to ourselves. + continue; + } + // Tell all existing instances to also close the banner, if they still + // exist. + // NOTE: If the user's new tab page is `about:tor`, this may include + // some preloaded pages that have not been made visible yet (see + // NewTabPagePreloading). + try { + actor.sendAsyncMessage("AboutTor:DismissYEC"); + } catch {} + } + break; + } + return undefined; + } +} diff --git a/browser/components/abouttor/HomepageOverride.sys.mjs b/browser/components/abouttor/HomepageOverride.sys.mjs @@ -0,0 +1,20 @@ +// FIXME: Eventually drop this entirely. It is only known to be used by Whonix, +// which could set their default home page using "browser.startup.homepage". +// See https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/13835#note_2928881 +export const HomepageOverride = { + check() { + // tor-browser#13835: Allow overriding the default homepage by setting a + // custom environment variable. + if (Services.env.exists("TOR_DEFAULT_HOMEPAGE")) { + const prefName = "browser.startup.homepage"; + // if the user has set this value in a previous installation, don't + // override it + if (!Services.prefs.prefHasUserValue(prefName)) { + Services.prefs.setCharPref( + prefName, + Services.env.get("TOR_DEFAULT_HOMEPAGE") + ); + } + } + }, +}; diff --git a/browser/components/abouttor/content/1f44b-waving-hand.svg b/browser/components/abouttor/content/1f44b-waving-hand.svg @@ -0,0 +1,3 @@ +<!-- FROM https://github.com/twitter/twemoji + - licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#EF9645" d="M4.861 9.147c.94-.657 2.357-.531 3.201.166l-.968-1.407c-.779-1.111-.5-2.313.612-3.093 1.112-.777 4.263 1.312 4.263 1.312-.786-1.122-.639-2.544.483-3.331 1.122-.784 2.67-.513 3.456.611l10.42 14.72L25 31l-11.083-4.042L4.25 12.625c-.793-1.129-.519-2.686.611-3.478z"/><path fill="#FFDC5D" d="M2.695 17.336s-1.132-1.65.519-2.781c1.649-1.131 2.78.518 2.78.518l5.251 7.658c.181-.302.379-.6.6-.894L4.557 11.21s-1.131-1.649.519-2.78c1.649-1.131 2.78.518 2.78.518l6.855 9.997c.255-.208.516-.417.785-.622L7.549 6.732s-1.131-1.649.519-2.78c1.649-1.131 2.78.518 2.78.518l7.947 11.589c.292-.179.581-.334.871-.498L12.238 4.729s-1.131-1.649.518-2.78c1.649-1.131 2.78.518 2.78.518l7.854 11.454 1.194 1.742c-4.948 3.394-5.419 9.779-2.592 13.902.565.825 1.39.26 1.39.26-3.393-4.949-2.357-10.51 2.592-13.903L24.515 8.62s-.545-1.924 1.378-2.47c1.924-.545 2.47 1.379 2.47 1.379l1.685 5.004c.668 1.984 1.379 3.961 2.32 5.831 2.657 5.28 1.07 11.842-3.94 15.279-5.465 3.747-12.936 2.354-16.684-3.11L2.695 17.336z"/><g fill="#5DADEC"><path d="M12 32.042C8 32.042 3.958 28 3.958 24c0-.553-.405-1-.958-1s-1.042.447-1.042 1C1.958 30 6 34.042 12 34.042c.553 0 1-.489 1-1.042s-.447-.958-1-.958z"/><path d="M7 34c-3 0-5-2-5-5 0-.553-.447-1-1-1s-1 .447-1 1c0 4 3 7 7 7 .553 0 1-.447 1-1s-.447-1-1-1zM24 2c-.552 0-1 .448-1 1s.448 1 1 1c4 0 8 3.589 8 8 0 .552.448 1 1 1s1-.448 1-1c0-5.514-4-10-10-10z"/><path d="M29 .042c-.552 0-1 .406-1 .958s.448 1.042 1 1.042c3 0 4.958 2.225 4.958 4.958 0 .552.489 1 1.042 1s.958-.448.958-1C35.958 3.163 33 .042 29 .042z"/></g></svg> diff --git a/browser/components/abouttor/content/1f4e3-megaphone.svg b/browser/components/abouttor/content/1f4e3-megaphone.svg @@ -0,0 +1,3 @@ +<!-- FROM https://github.com/twitter/twemoji + - licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#3B88C3" d="M14 19c-3.314 0-6 2.687-6 6s2.686 6 6 6 6-2.687 6-6-2.687-6-6-6zm0 10c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/><path fill="#55ACEE" d="M1.783 14.023v.02C.782 14.263 0 15.939 0 18s.782 3.737 1.783 3.956v.021l28.701 7.972V6.064L1.783 14.023z"/><ellipse fill="#269" cx="31" cy="18" rx="5" ry="12"/></svg> diff --git a/browser/components/abouttor/content/26a1-high-voltage.svg b/browser/components/abouttor/content/26a1-high-voltage.svg @@ -0,0 +1,3 @@ +<!-- FROM https://github.com/twitter/twemoji + - licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFAC33" d="M32.938 15.651C32.792 15.26 32.418 15 32 15H19.925L26.89 1.458c.219-.426.106-.947-.271-1.243C26.437.071 26.218 0 26 0c-.233 0-.466.082-.653.243L18 6.588 3.347 19.243c-.316.273-.43.714-.284 1.105S3.582 21 4 21h12.075L9.11 34.542c-.219.426-.106.947.271 1.243.182.144.401.215.619.215.233 0 .466-.082.653-.243L18 29.412l14.653-12.655c.317-.273.43-.714.285-1.106z"/></svg> diff --git a/browser/components/abouttor/content/2728-sparkles.svg b/browser/components/abouttor/content/2728-sparkles.svg @@ -0,0 +1,3 @@ +<!-- FROM https://github.com/twitter/twemoji + - licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFAC33" d="M34.347 16.893l-8.899-3.294-3.323-10.891c-.128-.42-.517-.708-.956-.708-.439 0-.828.288-.956.708l-3.322 10.891-8.9 3.294c-.393.146-.653.519-.653.938 0 .418.26.793.653.938l8.895 3.293 3.324 11.223c.126.424.516.715.959.715.442 0 .833-.291.959-.716l3.324-11.223 8.896-3.293c.391-.144.652-.518.652-.937 0-.418-.261-.792-.653-.938z"/><path fill="#FFCC4D" d="M14.347 27.894l-2.314-.856-.9-3.3c-.118-.436-.513-.738-.964-.738-.451 0-.846.302-.965.737l-.9 3.3-2.313.856c-.393.145-.653.52-.653.938 0 .418.26.793.653.938l2.301.853.907 3.622c.112.444.511.756.97.756.459 0 .858-.312.97-.757l.907-3.622 2.301-.853c.393-.144.653-.519.653-.937 0-.418-.26-.793-.653-.937zM10.009 6.231l-2.364-.875-.876-2.365c-.145-.393-.519-.653-.938-.653-.418 0-.792.26-.938.653l-.875 2.365-2.365.875c-.393.146-.653.52-.653.938 0 .418.26.793.653.938l2.365.875.875 2.365c.146.393.52.653.938.653.418 0 .792-.26.938-.653l.875-2.365 2.365-.875c.393-.146.653-.52.653-.938 0-.418-.26-.792-.653-.938z"/></svg> diff --git a/browser/components/abouttor/content/2764-red-heart.svg b/browser/components/abouttor/content/2764-red-heart.svg @@ -0,0 +1,3 @@ +<!-- FROM https://github.com/twitter/twemoji + - licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ --> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/></svg> diff --git a/browser/components/abouttor/content/aboutTor.css b/browser/components/abouttor/content/aboutTor.css @@ -0,0 +1,529 @@ +@import url("chrome://global/skin/tor-colors.css"); +@import url("chrome://global/skin/onion-pattern.css"); + +body { + margin: 0; + min-height: 100vh; + display: grid; + --form-max-width: 600px; + grid-template: + /* Start space: unfilled. */ + ". . ." 1fr + "heading heading heading" auto + "tor-check tor-check tor-check" auto + ". form ." min-content + "message message message" auto + "survey survey survey" auto + /* End space: unfilled. + * Reserve 150px for background image. + * NOTE: Since the body has "auto" height, the other "1fr" flex row will + * not shrink to zero, but will instead shrink to a minimum size of + * 75px = (150px * 1fr / 2fr) */ + ". . ." minmax(var(--onion-pattern-height), 2fr) + /* NOTE: "form" will be given a maximum width of --form-max-width. */ + / 1fr minmax(max-content, var(--form-max-width)) 1fr; + justify-items: center; + padding-inline: 20px; +} + +body:not(.initialized) { + /* Hide the components before the page is initialized. + * NOTE: The layout can still be adjusted or measured in this time since we + * use visibility rather than `display: none`. */ + visibility: hidden; +} + +h1 { + grid-area: heading; + display: flex; + align-items: center; + gap: 16px; + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-font-size-tokens */ + font-size: 40px; + margin-block-start: 0; + margin-block-end: 40px; + margin-inline: 20px; +} + +#tor-browser-logo { + height: 80px; + flex: 0 0 auto; +} + +body.is-testing #tor-browser-home-heading-stable { + display: none; +} + +body:not(.is-testing) #tor-browser-home-heading-testing { + display: none; +} + +#tor-check { + grid-area: tor-check; + display: flex; + gap: 10px; + align-items: center; + padding-inline: 23px; + padding-block: 11px; + border-radius: var(--border-radius-medium); + margin-block-start: 0; + margin-block-end: 30px; +} + +.tor-home-box { + border: 1px solid var(--border-color); + background-color: var(--background-color-box-info); + max-width: var(--form-max-width); + width: -moz-available; + box-sizing: border-box; +} + +body:not(.show-tor-check) #tor-check { + display: none; +} + +#tor-check-icon { + flex: 0 0 auto; + width: 16px; + height: 16px; + -moz-context-properties: fill; + fill: currentColor; +} + +.home-message:not(.shown-message) { + display: none; +} + +.home-message { + grid-area: message; + text-align: center; + margin-block: 1.6em; +} + +.message-emoji { + height: 1em; + vertical-align: sub; + margin-inline-end: 0.3em; +} + +#search-form { + grid-area: form; + /* Occupy the entire "form" block. */ + justify-self: stretch; + background: var(--background-color-canvas); + display: flex; + align-items: stretch; + /* Padding between elements. */ + --form-padding: 12px; + --form-border-width: 1px; + /* Padding between elements and the parent's border edge. */ + --form-outer-padding: calc(var(--form-padding) - var(--form-border-width)); + --form-radius-container: var(--border-radius-medium); + --form-radius: calc(var(--form-radius-container) - var(--form-border-width)); + --logo-size: 30px; + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-border-radius-tokens */ + border-radius: var(--form-radius-container); + border-width: var(--form-border-width); + border-style: solid; + border-color: var(--border-color); +} + +#search-form:has(#search-input:focus-visible) { + outline: var(--focus-outline); +} + +#dax-logo { + width: var(--logo-size); + flex: 0 0 auto; + align-self: center; + margin-inline-start: var(--form-outer-padding); + /* Does not occupy any layout width. */ + margin-inline-end: calc(-1 * (var(--logo-size) + var(--form-outer-padding))); +} + +#search-input { + flex: 1 0 auto; + min-width: 200px; + min-height: var(--logo-size); + box-sizing: content-box; + margin: 0; + padding-block: var(--form-outer-padding); + padding-inline-end: var(--form-padding); + padding-inline-start: calc(var(--form-outer-padding) + var(--logo-size) /* logo */ + var(--form-padding) /* padding after logo. */); + /* Make sure clickable area does not extend beyond the form's border. */ + /* stylelint-disable stylelint-plugin-mozilla/use-border-radius-tokens */ + border-start-start-radius: var(--form-radius); + border-end-start-radius: var(--form-radius); + border-start-end-radius: 0; + border-end-end-radius: 0; + /* stylelint-enable stylelint-plugin-mozilla/use-border-radius-tokens */ + /* Focus and outline styling move to the parent. */ + background: none; + border: none; + outline: none; +} + +#onionize-toggle { + flex: 0 0 auto; + align-content: center; + padding-block: var(--form-outer-padding); + padding-inline-end: var(--form-outer-padding); + /* stylelint-disable stylelint-plugin-mozilla/use-border-radius-tokens */ + border-start-end-radius: var(--form-radius); + border-end-end-radius: var(--form-radius); + border-start-start-radius: 0; + border-end-start-radius: 0; + /* stylelint-enable stylelint-plugin-mozilla/use-border-radius-tokens */ + padding-inline-start: 0; + /* Non-clickable gap between input and toggle. */ + margin-inline-start: 0.5em; +} + +#survey { + grid-area: survey; + display: grid; + grid-template: + "icon heading close" min-content + "icon body close" auto + ". buttons buttons" min-content + / min-content 1fr min-content; + border-radius: var(--border-radius-small); + /* Remove 1px from padding for border. */ + padding-block: 3px 11px; + padding-inline: 15px 3px; + gap: 8px; + margin-block-end: 1.6em; +} + +body:not(.show-survey) #survey { + display: none; +} + +#survey > * { + margin: 0; +} + +#survey-icon { + grid-area: icon; + width: 24px; + height: 24px; + padding: 8px; + border-radius: var(--border-radius-circle); +} + +#survey-heading { + grid-area: heading; + font-size: inherit; +} + +#survey-icon, +#survey-heading { + margin-block-start: 8px; +} + +#survey-body { + grid-area: body; + margin-block-end: 8px; +} + +#survey-buttons { + grid-area: buttons; + display: flex; + gap: 8px; +} + +#survey-buttons > * { + flex: 0 0 auto; + margin: 0; +} + +#survey-close { + grid-area: close; +} + +@media not ((prefers-contrast) or (forced-colors)) { + /* Force the page to follow the same Tor theme, regardless of + * prefers-color-scheme. */ + + /* On dark background */ + :root { + background-color: #2c0449; + --focus-outline-color: var(--tor-focus-outline-color-dark); + --focus-outline: var(--focus-outline-width) solid var(--focus-outline-color); + --onion-pattern-stroke-color: #3e0663; + --onion-pattern-fill-color: #350556; + /* Same as --text-color when "prefers-color-scheme: light" */ + --text-color-light: var(--color-gray-100); + /* Same as --text-color when "prefers-color-scheme: dark" */ + --text-color-dark: var(--color-gray-05); + } + + #tor-check { + background-color: #1f0333; + border-color: transparent; + } + + body > :not(#search-form) { + color: var(--text-color-dark); + --button-text-color: currentColor; + --button-text-color-hover: var(--button-text-color); + --button-text-color-active: var(--button-text-color); + --button-text-color-ghost: var(--button-text-color); + --button-text-color-ghost-hover: var(--button-text-color); + --button-text-color-ghost-active: var(--button-text-color); + --link-color: var(--tor-link-color-dark); + --link-color-hover: var(--tor-link-color-hover-dark); + --link-color-active: var(--tor-link-color-active-dark); + } + + #search-form { + /* Use light color for background and moz-toggle shadow root. */ + color-scheme: light; + color: var(--text-color-light); + border-color: transparent; + } + + #search-form:has(#search-input:focus-visible) { + /* Use a light-themed inner-border to contrast with the dark-themed + * focus outline. */ + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-border-color-tokens */ + border-color: var(--tor-focus-outline-color-light); + } + + #search-form.onionized-search:has(#search-input:not(:focus-visible)) { + box-shadow: + 0 4px 40px #9400ff66, + 0 4px 16px #9400ff33; + } + + /* Light background. */ + #search-form > * { + --focus-outline-color: var(--tor-focus-outline-color-light); + --focus-outline: var(--focus-outline-width) solid var(--focus-outline-color); + /* Variables used for --toggle- variables. */ + --color-accent-primary: var(--tor-button-background-color-light); + --color-accent-primary-hover: var(--tor-button-background-color-hover-light); + --color-accent-primary-active: var(--tor-button-background-color-active-light); + } + + #search-form.onionized-search #onionize-toggle { + color: var(--tor-link-color-light); + } + + #survey { + background-color: #3d1559; + border-color: transparent; + } + + #survey-icon { + background-color: #00000040; + } + + #survey-launch { + color: var(--text-color-light); + --button-background-color-primary: var(--tor-button-background-color-dark); + --button-background-color-primary-hover: var(--tor-button-background-color-hover-dark); + --button-background-color-primary-active: var(--tor-button-background-color-active-dark); + } +} + +/* Year End Campaign (YEC). */ +body:not(.show-yec) #yec-banner { + display: none; +} + +body.show-yec h1 { + /* Text of heading is still available to screen readers, but it does not + * contribute visually to the page or the body grid layout. */ + position: absolute; + clip-path: inset(50%); +} + +#yec-banner { + grid-area: heading; + border-radius: var(--border-radius-medium); + border: 1px solid var(--border-color); + display: grid; + grid-template: + "yec-heading yec-image" auto + "yec-body yec-image" auto + "yec-matching yec-image" auto + "yec-donate yec-image" min-content + / 1fr min-content; + --yec-image-background: #1f0333; + /* Remove 1px from padding for border. */ + padding-block: 55px 55px; + padding-inline: 47px 47px; + box-sizing: border-box; + max-width: 850px; + margin-block-end: 40px; + /* Position for the close button. */ + position: relative; + gap: 0 24px; +} + +#yec-image { + grid-area: yec-image; + align-self: center; + /* Center horizontally for small width layout. */ + justify-self: center; + /* background color, padding and border radius only stands out when using a + * contrast or forced color theme. */ + background-color: var(--yec-image-background); + border-radius: var(--border-radius-medium); + padding: 4px; + height: 196px; + border: 1px solid transparent; + /* Remove border and padding from the layout size. These parts are only + * visible in contrast or forced color themes. */ + margin: -5px; + /* Do not let forced colors ignore the background-color, which is needed to + * see the drawing. */ + forced-color-adjust: none; +} + +#yec-image:dir(rtl) { + transform: scaleX(-1); +} + +#yec-heading { + grid-area: yec-heading; + margin-block: 0 16px; + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-font-size-tokens */ + font-size: 64px; + font-weight: var(--font-weight); +} + +#yec-body { + grid-area: yec-body; + margin-block: 0 12px; +} + +#yec-body-highlight { + font-weight: var(--font-weight-bold); +} + +#yec-matching { + grid-area: yec-matching; + margin-block: 0 32px; +} + +#yec-donate-link { + grid-area: yec-donate; + justify-self: start; + /* Style like a button. */ + font-weight: var(--button-font-weight); + color: var(--button-text-color); + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + background-color: var(--button-background-color); + padding: var(--button-padding); + box-sizing: border-box; + min-height: var(--button-min-height); + text-decoration: none; + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} + +#yec-donate-link > * { + flex: 0 0 auto; +} + +#yec-donate-link:hover { + background-color: var(--button-background-color-hover); + color: var(--button-text-color-hover); + border-color: var(--button-border-color-hover); +} + +#yec-donate-link:hover:active { + background-color: var(--button-background-color-active); + color: var(--button-text-color-active); + border-color: var(--button-border-color-active); +} + +#yec-donate-icon { + -moz-context-properties: fill; + fill: currentColor; +} + +#yec-close { + position: absolute; + inset-block-start: 16px; + inset-inline-end: 16px; +} + +@media (max-width: 768px) { + /* Small width layout. */ + #yec-banner { + grid-template: + "yec-image" min-content + "yec-heading" auto + "yec-body" auto + "yec-matching" auto + "yec-donate" min-content + / 1fr; + padding-block: 31px 39px; + padding-inline: 15px; + /* Match max-width of the form. */ + max-width: var(--form-max-width); + margin-block-end: 32px; + } + + #yec-image { + height: 156px; + } + + #yec-heading { + margin-block-start: 16px; + text-align: center; + text-wrap-style: balance; + } + + #yec-heading { + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-font-size-tokens */ + font-size: 50px; + } + + #yec-matching { + margin-block-end: 16px; + } + + #yec-donate-link { + justify-self: center; + } +} + +@media ((prefers-contrast) or (forced-colors)) and (prefers-color-scheme: dark) { + #yec-image { + /* Give the dark image a light border to separate from background. */ + border-color: var(--border-color); + } +} + +@media not ((prefers-contrast) or (forced-colors)) { + #yec-banner { + border-color: transparent; + background-color: var(--yec-image-background); + --yec-text: #ffffff; + --yec-button-background: #b6e368; + --yec-button-background-hover: #d2f2a1; + --yec-button-background-active: #ecfcd8; + --yec-button-text: #15141a; + color: var(--yec-text); + } + + #yec-body-highlight { + color: var(--yec-button-background); + } + + #yec-donate-link { + --button-text-color: var(--yec-button-text); + --button-text-color-hover: var(--yec-button-text); + --button-text-color-active: var(--yec-button-text); + --button-background-color: var(--yec-button-background); + --button-background-color-hover: var(--yec-button-background-hover); + --button-background-color-active: var(--yec-button-background-active); + } +} diff --git a/browser/components/abouttor/content/aboutTor.html b/browser/components/abouttor/content/aboutTor.html @@ -0,0 +1,187 @@ +<!doctype html> + +<html> + <head> + <meta + http-equiv="Content-Security-Policy" + content="default-src chrome:; object-src 'none'" + /> + <title data-l10n-id="newtab-page-title"></title> + <link + rel="icon" + type="image/png" + href="chrome://branding/content/icon32.png" + /> + <!-- We need common.css to get styling for the moz-toggle. --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <link + rel="stylesheet" + href="chrome://browser/content/abouttor/aboutTor.css" + /> + + <link rel="localization" href="browser/newtab/newtab.ftl" /> + <link rel="localization" href="toolkit/global/tor-browser.ftl" /> + + <script + type="module" + src="chrome://global/content/elements/moz-button.mjs" + ></script> + <script + type="module" + src="chrome://global/content/elements/moz-toggle.mjs" + ></script> + <script src="chrome://browser/content/abouttor/aboutTor.js"></script> + </head> + <body class="onion-pattern-background"> + <!-- Year End Campaign (YEC). --> + <article id="yec-banner" aria-labelledby="yec-heading"> + <img id="yec-image" alt="" /> + <h2 id="yec-heading"></h2> + <p id="yec-body"> + <b id="yec-body-highlight" data-l10n-name="highlight"></b> + </p> + <p id="yec-matching"></p> + <a id="yec-donate-link"> + <span data-l10n-id="yec-donate-button"></span> + <img + id="yec-donate-icon" + alt="" + src="chrome://browser/content/abouttor/yec-heart.svg" + /> + </a> + <moz-button + id="yec-close" + type="icon ghost" + class="close" + size="16" + iconSrc="chrome://global/skin/icons/close.svg" + data-l10n-id="yec-close-button" + ></moz-button> + </article> + <h1> + <img + id="tor-browser-logo" + alt="" + src="chrome://branding/content/about-logo.svg" + /> + <span + id="tor-browser-home-heading-stable" + data-l10n-id="tor-browser-home-heading-stable" + ></span> + <span + id="tor-browser-home-heading-testing" + data-l10n-id="tor-browser-home-heading-testing" + ></span> + </h1> + <p id="tor-check" class="tor-home-box"> + <img + id="tor-check-icon" + alt="" + src="chrome://global/skin/icons/info.svg" + /> + <span data-l10n-id="tor-browser-home-tor-check-warning"> + <a + data-l10n-name="tor-check-link" + href="https://check.torproject.org/" + target="_blank" + ></a> + </span> + </p> + <form id="search-form" method="get" rel="noreferrer"> + <img + id="dax-logo" + alt="" + src="chrome://browser/content/abouttor/dax-logo.svg" + /> + <input + id="search-input" + name="q" + autocomplete="off" + type="text" + data-l10n-id="tor-browser-home-duck-duck-go-input" + /> + <moz-toggle + id="onionize-toggle" + label-align-before="" + data-l10n-id="tor-browser-home-onionize-toggle" + data-l10n-attrs="label" + ></moz-toggle> + </form> + <p id="home-message-updated" class="home-message"> + <img + class="message-emoji" + alt="" + src="chrome://browser/content/abouttor/2728-sparkles.svg" + /> + <span> + <a data-l10n-name="update-link" target="_blank"></a> + </span> + </p> + <p class="home-message home-message-rotating-stable"> + <span data-l10n-id="tor-browser-home-message-introduction"></span> + </p> + <p class="home-message home-message-rotating-stable"> + <img + class="message-emoji" + alt="" + src="chrome://browser/content/abouttor/2764-red-heart.svg" + /> + <span data-l10n-id="tor-browser-home-message-donate"> + <a + data-l10n-name="donate-link" + href="https://donate.torproject.org" + target="_blank" + ></a> + </span> + </p> + <p class="home-message home-message-rotating-stable"> + <img + class="message-emoji" + alt="" + src="chrome://browser/content/abouttor/1f4e3-megaphone.svg" + /> + <span data-l10n-id="tor-browser-home-message-news"> + <a + data-l10n-name="news-link" + href="https://newsletter.torproject.org" + target="_blank" + ></a> + </span> + </p> + <p class="home-message home-message-rotating-testing"> + <img + class="message-emoji" + alt="" + src="chrome://browser/content/abouttor/26a1-high-voltage.svg" + /> + <span data-l10n-id="tor-browser-home-message-testing"> + <a + data-l10n-name="learn-more-link" + href="https://community.torproject.org/user-research/become-tester/" + target="_blank" + ></a> + </span> + </p> + <!-- Survey element, initially used for tor-browser#43504. --> + <article id="survey" class="tor-home-box" aria-labelledby="survey-heading"> + <img + id="survey-icon" + alt="" + src="chrome://browser/content/abouttor/1f44b-waving-hand.svg" + /> + <h2 id="survey-heading"></h2> + <p id="survey-body"></p> + <div id="survey-buttons"> + <button id="survey-launch" class="primary"></button> + <button id="survey-dismiss"></button> + </div> + <moz-button + id="survey-close" + type="icon ghost" + class="close" + size="16" + iconSrc="chrome://global/skin/icons/close.svg" + ></moz-button> + </article> + </body> +</html> diff --git a/browser/components/abouttor/content/aboutTor.js b/browser/components/abouttor/content/aboutTor.js @@ -0,0 +1,590 @@ +"use strict"; + +const SearchWidget = { + _initialized: false, + _initialOnionize: false, + + /** + * Initialize the search form elements. + */ + init() { + this._initialized = true; + + this.searchForm = document.getElementById("search-form"); + this.onionizeToggle = document.getElementById("onionize-toggle"); + this.onionizeToggle.pressed = this._initialOnionize; + this._updateOnionize(); + this.onionizeToggle.addEventListener("toggle", () => + this._updateOnionize() + ); + + // If the user submits, save the onionize search state for the next about:tor + // page. + this.searchForm.addEventListener("submit", () => { + dispatchEvent( + new CustomEvent("SubmitSearchOnionize", { + detail: this.onionizeToggle.pressed, + bubbles: true, + }) + ); + }); + + // By default, Enter on the onionizeToggle will toggle the button rather + // than submit the <form>. + // Moreover, our <form> has no submit button, so can only be submitted by + // pressing Enter. + // For keyboard users, Space will also toggle the form. We do not want to + // require users to have to Tab back to the search input in order to press + // Enter to submit the form. + // For mouse users, clicking the toggle button will give it focus, so they + // would have to Tab back or click the search input in order to submit the + // form. + // So we want to intercept the Enter keydown event to submit the form. + this.onionizeToggle.addEventListener( + "keydown", + event => { + if (event.key !== "Enter") { + return; + } + event.preventDefault(); + event.stopPropagation(); + this.searchForm.requestSubmit(); + }, + { capture: true } + ); + }, + + _updateOnionize() { + // Change submit URL based on the onionize toggle. + this.searchForm.action = this.onionizeToggle.pressed + ? "https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion" + : "https://duckduckgo.com"; + this.searchForm.classList.toggle( + "onionized-search", + this.onionizeToggle.pressed + ); + }, + + /** + * Set what the "Onionize" toggle state. + * + * @param {boolean} state - Whether the "Onionize" toggle should be switched + * on. + */ + setOnionizeState(state) { + if (!this._initialized) { + this._initialOnionize = state; + return; + } + this.onionizeToggle.pressed = state; + this._updateOnionize(); + }, +}; + +const MessageArea = { + _initialized: false, + _messageData: null, + _isStable: null, + _torConnectEnabled: null, + + /** + * Initialize the message area and heading once elements are available. + */ + init() { + this._initialized = true; + this._update(); + }, + + /** + * Set the message data and stable release flag. + * + * @param {MessageData} messageData - The message data, indicating which + * message to show. + * @param {boolean} isStable - Whether this is the stable release version. + * @param {boolean} torConnectEnabled - Whether TorConnect is enabled, and + * therefore the Tor process was configured with about:torconnect. + */ + setMessageData(messageData, isStable, torConnectEnabled) { + this._messageData = messageData; + this._isStable = isStable; + this._torConnectEnabled = torConnectEnabled; + this._update(); + }, + + _update() { + if (!this._initialized) { + return; + } + + document + .querySelector(".home-message.shown-message") + ?.classList.remove("shown-message"); + + if (!this._messageData) { + return; + } + + // Set heading. + document.body.classList.toggle("is-testing", !this._isStable); + + document.body.classList.toggle("show-tor-check", !this._torConnectEnabled); + + const { updateVersion, updateURL, number } = this._messageData; + + if (updateVersion) { + const updatedElement = document.getElementById("home-message-updated"); + updatedElement.querySelector("a").href = updateURL; + document.l10n.setAttributes( + updatedElement.querySelector("span"), + "tor-browser-home-message-updated", + { version: updateVersion } + ); + updatedElement.classList.add("shown-message"); + } else { + const messageElements = document.querySelectorAll( + this._isStable + ? ".home-message-rotating-stable" + : ".home-message-rotating-testing" + ); + messageElements[number % messageElements.length].classList.add( + "shown-message" + ); + } + }, +}; + +/** + * A reusable area for surveys. + * + * Initially used for tor-browser#43504. + */ +const SurveyArea = { + /** + * The current version of the survey. + * + * Should be increased every time we start a new survey campaign. + * + * @type {integer} + */ + _version: 1, + + /** + * The date to start showing the survey. + * + * @type {?integer} + */ + _startDate: null, // No survey date. + + /** + * The date to stop showing the current survey. + * + * @type {?integer} + */ + _endDate: null, // No survey date. + + /** + * The survey URL. + * + * @type {string} + */ + _urlBase: "https://survey.torproject.org/index.php/923269", + + /** + * @typedef {object} SurveyLocaleData + * + * Locale-specific data for the survey. + * + * @property {string[]} browserLocales - The browser locales this should match + * with. The first locale should match the locale of the strings. + * @property {string} urlCode - The language code to pass to the survey URL. + * @property {string} dir - The direction of the locale. + * @property {object} strings - The strings to use for the survey banner. + */ + + /** + * The data for the selected locale. + * + * @type {SurveyLocaleData} + */ + _localeData: null, + + /** + * The data for each locale that is supported. + * + * The first entry is the default. + * + * @type {SurveyLocaleData[]} + */ + _localeDataSet: [ + { + browserLocales: ["en-US"], + dir: "ltr", + urlCode: "en", + strings: { + heading: "We’d love your feedback", + body: "Help us improve Tor Browser by completing this 10-minute survey.", + launch: "Launch the survey", + dismiss: "Dismiss", + close: "Close", + }, + }, + { + browserLocales: ["es-ES"], + dir: "ltr", + urlCode: "es", + strings: { + heading: "Danos tu opinión", + body: "Ayúdanos a mejorar el Navegador Tor completando esta encuesta de 10 minutos.", + launch: "Iniciar la encuesta", + dismiss: "Descartar", + close: "Cerrar", + }, + }, + { + browserLocales: ["ru"], + dir: "ltr", + urlCode: "ru", + strings: { + heading: "Мы будем рады вашим отзывам", + body: "Помогите нам улучшить браузер Tor, пройдя 10-минутный опрос.", + launch: "Начать опрос", + dismiss: "Отклонить", + close: "Закрыть", + }, + }, + { + browserLocales: ["fr"], + dir: "ltr", + urlCode: "fr", + strings: { + heading: "Nous serions ravis d’avoir votre avis !", + body: "Aidez-nous à améliorer le navigateur Tor en répondant à cette enquête de 10 minutes.", + launch: "Lancer l'enquête", + dismiss: "Ignorer", + close: "Fermer", + }, + }, + { + // Also show this pt-BR banner for the pt-PT browser locale. + browserLocales: ["pt-BR", "pt-PT"], + dir: "ltr", + urlCode: "pt-BR", + strings: { + heading: "Adoraríamos ouvir sua opinião", + body: "Ajude-nos a melhorar o Navegador Tor respondendo a esta pesquisa de 10 minutos.", + launch: "Iniciar a pesquisa", + dismiss: "Dispensar", + close: "Fechar", + }, + }, + ], + + /** + * Whether the area has been initialised. + * + * @type {boolean} + */ + _initialized: false, + + /** + * The app locale, or `null` whilst unset. + * + * @type {?string} + */ + _locale: null, + + /** + * Whether the banner should be shown. + * + * @type {boolean} + */ + _shouldShow: false, + + /** + * The survey element. + * + * @type {?Element} + */ + _areaEl: null, + + /** + * Initialize the survey area. + */ + init() { + this._initialized = true; + + this._areaEl = document.getElementById("survey"); + document.getElementById("survey-launch").addEventListener("click", () => { + const url = URL.parse(this._urlBase); + if (!url || !this._localeData) { + return; + } + + url.searchParams.append("lang", this._localeData.urlCode); + open(url.href); + }); + document.getElementById("survey-close").addEventListener("click", () => { + this._hide(); + }); + document.getElementById("survey-dismiss").addEventListener("click", () => { + this._hide(); + }); + + this._update(); + }, + + /** + * Permanently hide this survey. + */ + _hide() { + this._shouldShow = false; + this._update(); + + dispatchEvent( + new CustomEvent("SurveyDismissed", { + // We pass in the current survey version to record the *latest* + // version that the user has dismissed. This will overwrite any + // previous versions. + detail: this._version, + bubbles: true, + }) + ); + }, + + /** + * Decide whether to show the survey. + * + * @param {integer} dismissVersion - The latest version of survey that the + * user has already dismissed. + * @param {boolean} isStable - Whether this is the stable release of Tor + * Browser. + * @param {string} appLocale - The app locale currently in use. + */ + potentiallyShow(dismissVersion, isStable, appLocale) { + const now = Date.now(); + this._shouldShow = + isStable && + dismissVersion < this._version && + this._startDate && + now >= this._startDate && + now < this._endDate; + this._locale = appLocale; + this._update(); + }, + + /** + * Update the display. + */ + _update() { + if (!this._initialized) { + return; + } + if (!this._shouldShow) { + if (this._areaEl.contains(document.activeElement)) { + // Move focus to the search input. + document.getElementById("search-input").focus(); + } + document.body.classList.remove("show-survey"); + return; + } + + // Determine the survey locale based on the app locale. + // NOTE: We do not user document.l10n to translate the survey banner. + // Instead we only translate the banner into a limited set of locales that + // match the languages that the survey itself supports. This should match + // the language of the survey when it is opened by the user. + for (const localeData of this._localeDataSet) { + if (localeData.browserLocales.includes(this._locale)) { + this._localeData = localeData; + break; + } + } + if (!this._localeData) { + // Show the default en-US banner. + this._localeData = this._localeDataSet[0]; + } + + // Make sure the survey's lang and dir attributes match the chosen locale. + const surveyEl = document.getElementById("survey"); + surveyEl.setAttribute("lang", this._localeData.browserLocales[0]); + surveyEl.setAttribute("dir", this._localeData.dir); + + const { heading, body, launch, dismiss, close } = this._localeData.strings; + + document.getElementById("survey-heading").textContent = heading; + document.getElementById("survey-body").textContent = body; + document.getElementById("survey-launch").textContent = launch; + document.getElementById("survey-dismiss").textContent = dismiss; + document.getElementById("survey-close").setAttribute("title", close); + + document.body.classList.add("show-survey"); + }, +}; + +/** + * Area for the Year End Campaign (YEC). + * See tor-browser#42072. + */ +const YecArea = { + /** + * The epoch time to start showing the banner, if at all. + * + * @type {?integer} + */ + _startDate: null, // No YEC is active. + + /** + * The epoch time to stop showing the banner, if at all. + * + * @type {?integer} + */ + _endDate: null, // No YEC is active. + + /** + * Whether the area has been initialised. + * + * @type {boolean} + */ + _initialized: false, + + /** + * The app locale, or `null` whilst unset. + * + * @type {?string} + */ + _locale: null, + + /** + * Whether the banner should be shown. + * + * @type {boolean} + */ + _shouldShow: false, + + /** + * The banner element. + * + * @type {?Element} + */ + _areaEl: null, + + /** + * Initialize the widget. + */ + init() { + this._initialized = true; + + this._areaEl = document.getElementById("yec-banner"); + + document.getElementById("yec-close").addEventListener("click", () => { + this.dismiss(); + dispatchEvent(new CustomEvent("UserDismissedYEC", { bubbles: true })); + }); + + this._update(); + }, + + /** + * Close the banner. + */ + dismiss() { + this._shouldShow = false; + this._update(); + }, + + /** + * Possibly show the banner. + * + * @param {boolean} dismissYEC - Whether the user has dismissed YEC. + * @param {boolean} isStable - Whether this is a stable release. + * @param {string} appLocale - The app locale, as BCP47. + */ + potentiallyShow(dismissYEC, isStable, appLocale) { + const now = Date.now(); + this._shouldShow = + !dismissYEC && + isStable && + this._startDate && + now >= this._startDate && + now < this._endDate; + this._locale = appLocale; + this._update(); + }, + + /** + * Update the visibility of the banner to reflect the new state. + */ + _update() { + if (!this._initialized) { + return; + } + if (!this._shouldShow) { + if (this._areaEl.contains(document.activeElement)) { + document.documentElement.focus(); + } + document.body.classList.remove("show-yec"); + return; + } + + const donateLink = document.getElementById("yec-donate-link"); + const base = "https://www.torproject.org/donate"; + donateLink.href = base; + + document.body.classList.add("show-yec"); + }, +}; + +let gInitialData = false; +let gLoaded = false; + +function maybeComplete() { + if (!gInitialData || !gLoaded) { + return; + } + // Wait to show the content when the l10n population has completed. + if (document.hasPendingL10nMutations) { + window.addEventListener( + "L10nMutationsFinished", + () => { + document.body.classList.add("initialized"); + }, + { once: true } + ); + } else { + document.body.classList.add("initialized"); + } +} + +window.addEventListener("DOMContentLoaded", () => { + SearchWidget.init(); + MessageArea.init(); + SurveyArea.init(); + YecArea.init(); + + gLoaded = true; + maybeComplete(); +}); + +window.addEventListener("InitialData", event => { + const { + torConnectEnabled, + isStable, + searchOnionize, + messageData, + surveyDismissVersion, + appLocale, + dismissYEC, + } = event.detail; + SearchWidget.setOnionizeState(!!searchOnionize); + MessageArea.setMessageData(messageData, !!isStable, !!torConnectEnabled); + SurveyArea.potentiallyShow(surveyDismissVersion, isStable, appLocale); + YecArea.potentiallyShow(dismissYEC, isStable, appLocale); + + gInitialData = true; + maybeComplete(); +}); + +window.addEventListener("DismissYEC", () => { + // User closed the banner in another about:tor instance. + YecArea.dismiss(); +}); diff --git a/browser/components/abouttor/content/dax-logo.svg b/browser/components/abouttor/content/dax-logo.svg @@ -0,0 +1 @@ +<svg width="120" height="120" viewbox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="50.833" cy="50.833" r="50.25"/><linearGradient x1="3.084%" y1="49.368%" x2="100.592%" y2="49.368%" id="c"><stop stop-color="#6176B9" offset=".56%"/><stop stop-color="#394A9F" offset="69.1%"/></linearGradient><linearGradient x1="-.006%" y1="49.006%" x2="98.932%" y2="49.006%" id="d"><stop stop-color="#6176B9" offset=".56%"/><stop stop-color="#394A9F" offset="69.1%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><circle fill="#FFF" cx="60" cy="60" r="57.5"/><ellipse fill="#DE5833" cx="60" cy="60" rx="50.25" ry="50.25"/><path d="M60 120C26.917 120 0 93.083 0 60S26.917 0 60 0s60 26.917 60 60-26.917 60-60 60zM60 4.917C29.667 4.917 4.917 29.583 4.917 60c0 30.333 24.666 55.083 55.083 55.083 30.417 0 55.083-24.666 55.083-55.083 0-30.333-24.75-55.083-55.083-55.083z" fill="#DE5833"/><g transform="translate(9.167 9.167)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#DE5833" xlink:href="#a"/><g mask="url(#b)"><path d="M71.917 127.25c-1.75-8.25-12.25-27.167-16.334-35.083-3.916-7.917-7.916-19.084-6.083-26.417.417-1.417-3.333-11.417-2.333-12 8.5-5.5 10.666.583 14-1.75 1.75-1.167 4.166 1 4.75-1 2.166-7.667-2.917-20.917-8.834-26.583-2-2-4.75-3.167-8.083-3.75-1.167-1.75-3.333-3.334-6.083-4.917-3.167-1.75-10.25-3.917-13.834-4.5-2.583-.417-3.166.167-4.166.417 1 0 5.75 2.333 6.666 2.583-1 .583-3.583 0-5.333.75-.75.417-1.583 2-1.583 2.583 4.916-.583 12.583 0 17.166 2-3.583.417-9.083.75-11.416 2.167-6.917 3.583-9.834 12-8.084 22.25 1.75 10.083 9.834 47.083 12.25 59.333 2.584 12.25-5.5 20.334-10.416 22.5l5.5.417-1.75 3.917c6.5.75 13.833-1.417 13.833-1.417-1.417 3.917-11.25 5.5-11.25 5.5s4.75 1.417 12.25-1.417c7.667-2.916 12.25-4.75 12.25-4.75l7.5 9.417 2.917-6.917 6.916 7.25c-.25-.5 1.334-2.25-.416-10.583z" fill="#D5D7D8"/><path d="M74.083 125.5c-1.75-8.25-12.25-27.167-16.333-35.083C53.833 82.5 49.833 71.333 51.667 64c.416-1.417.416-6.667 1.416-7.5 8.5-5.5 7.917-.167 11.25-2.583 1.75-1.167 3.167-2.75 3.75-4.917 2.167-7.667-2.916-20.917-8.833-26.583-2-2-4.75-3.167-8.083-3.75-1.167-1.75-3.334-3.334-6.084-4.917-5.333-2.917-12-3.917-18.333-2.917 1 0 3.333 2.167 4.167 2.334-1.417 1-5.084.75-5.084 2.916 4.917-.416 10.25.167 15 2.334-3.583.416-6.916 1.416-9.25 2.583-6.916 3.583-8.666 10.833-6.916 20.917C26.417 52 34.5 89 36.917 101.25c2.583 12.25-5.5 20.333-10.417 22.5l5.5.417-1.75 3.916c6.5.75 13.833-1.416 13.833-1.416-1.416 3.916-11.25 5.5-11.25 5.5s4.75 1.416 12.25-1.417c7.667-2.917 12.25-4.75 12.25-4.75l3.584 9.417 6.916-6.917 2.917 7.25c-.417 0 5.083-1.75 3.333-10.25z" fill="#FFF"/><path d="M32.5 42.583c0-2.166 1.75-3.75 3.75-3.75 2.167 0 3.75 1.75 3.75 3.75 0 2.167-1.75 3.75-3.75 3.75-2.167 0-3.75-1.583-3.75-3.75z" fill="#2D4F8E"/><path d="M36.833 41.167c0-.584.417-1 1-1 .584 0 1 .416 1 1 0 .583-.416 1-1 1-.416 0-1-.417-1-1z" fill="#FFF"/><path d="M58.333 40.167c0-1.75 1.417-3.334 3.334-3.334 1.75 0 3.333 1.417 3.333 3.334 0 1.75-1.417 3.333-3.333 3.333-1.75.083-3.334-1.333-3.334-3.333z" fill="#2D4F8E"/><path d="M62.25 39.167c0-.417.417-.75.75-.75.417 0 .75.416.75.75 0 .416-.417.75-.75.75-.333.083-.75-.334-.75-.75z" fill="#FFF"/><path d="M15.583 21.5s-2.916-1.417-5.75.417c-2.75 1.75-2.75 3.583-2.75 3.583S5.5 22.167 9.417 20.583c4.416-1.583 6.166.917 6.166.917z" fill="url(#c)" transform="translate(21.667 10)"/><path d="M42 21.333s-2-1.166-3.75-1.166c-3.333 0-4.167 1.583-4.167 1.583s.584-3.583 4.75-2.75c2.334.167 3.167 2.333 3.167 2.333z" fill="url(#d)" transform="translate(21.667 10)"/><path d="M47.917 57.167c.416-2.334 6.333-6.667 10.416-6.917 4.167-.167 5.5-.167 9.084-1C71 48.5 80 46.083 82.583 44.917c2.584-1.167 13.167.583 5.75 4.75-3.166 1.75-12 5.083-18.333 7.083-6.333 1.75-10.083-1.75-12 1.417-1.583 2.333-.417 5.75 7.083 6.5 10.084 1 19.667-4.5 20.667-1.584 1 2.917-8.667 6.5-14.583 6.667-5.917.167-17.917-3.917-19.667-5.083-1.833-1.584-4.25-4.417-3.583-7.5z" fill="#FDD20A"/></g></g><path d="M61.583 94.917s-14.166-7.5-14.416-4.5C47 93.583 47.167 106 48.75 107c1.583 1 13.417-6.083 13.417-6.083l-.584-6zm5.5-.667S76.75 87 78.917 87.333c2.166.417 2.583 15.584.75 16.334-1.917.833-13-3.667-13-3.667l.416-5.75z" fill="#65BC46"/><path d="M58.25 95.667c0 4.916-.75 7.083 1.417 7.5 2.166.416 6.083 0 7.5-1 1.416-1 .166-7.25-.167-8.5-.667-1.167-8.75-.167-8.75 2z" fill="#43A244"/><path d="M59 94.5c0 4.917-.75 7.083 1.417 7.5 2.166.417 6.083 0 7.5-1 1.416-1 .166-7.25-.167-8.5-.5-1-8.75-.167-8.75 2z" fill="#65BC46"/></g></svg> +\ No newline at end of file diff --git a/browser/components/abouttor/content/yec-heart.svg b/browser/components/abouttor/content/yec-heart.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="context-fill" xmlns="http://www.w3.org/2000/svg"> + <path d="M8 6C8 6 8 2 11.5 2C15 2 15 5 15 6C15 10.5 8 15 8 15V6Z"/> + <path d="M8 6C8 6 8 2 4.5 2C1 2 1 5 1 6C1 10.5 8 15 8 15L9 9L8 6Z"/> +</svg> diff --git a/browser/components/abouttor/jar.mn b/browser/components/abouttor/jar.mn @@ -0,0 +1,11 @@ +browser.jar: + content/browser/abouttor/aboutTor.css (content/aboutTor.css) + content/browser/abouttor/aboutTor.js (content/aboutTor.js) + content/browser/abouttor/aboutTor.html (content/aboutTor.html) + content/browser/abouttor/dax-logo.svg (content/dax-logo.svg) + content/browser/abouttor/1f44b-waving-hand.svg (content/1f44b-waving-hand.svg) + content/browser/abouttor/1f4e3-megaphone.svg (content/1f4e3-megaphone.svg) + content/browser/abouttor/26a1-high-voltage.svg (content/26a1-high-voltage.svg) + content/browser/abouttor/2728-sparkles.svg (content/2728-sparkles.svg) + content/browser/abouttor/2764-red-heart.svg (content/2764-red-heart.svg) + content/browser/abouttor/yec-heart.svg (content/yec-heart.svg) diff --git a/browser/components/abouttor/moz.build b/browser/components/abouttor/moz.build @@ -0,0 +1,11 @@ +JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES += [ + "AboutTorMessage.sys.mjs", + "HomepageOverride.sys.mjs", +] + +FINAL_TARGET_FILES.actors += [ + "AboutTorChild.sys.mjs", + "AboutTorParent.sys.mjs", +] diff --git a/browser/components/moz.build b/browser/components/moz.build @@ -29,6 +29,7 @@ with Files("controlcenter/**"): DIRS += [ "about", "aboutlogins", + "abouttor", "aboutwelcome", "aiwindow", "asrouter", diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml @@ -35,6 +35,7 @@ class="check-home-page-controlled" data-preference-related="browser.startup.homepage"> <menupopup> + <menuitem value="0" data-l10n-id="home-mode-choice-tor" /> <menuitem value="2" data-l10n-id="home-mode-choice-custom" /> <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> </menupopup> @@ -83,6 +84,7 @@ Preferences so we need to handle setting the pref manually.--> <menulist id="newTabMode" flex="1" data-preference-related="browser.newtabpage.enabled"> <menupopup> + <menuitem value="0" data-l10n-id="home-mode-choice-tor" /> <menuitem value="1" data-l10n-id="home-mode-choice-blank" /> </menupopup> </menulist> diff --git a/browser/components/tabbrowser/NewTabPagePreloading.sys.mjs b/browser/components/tabbrowser/NewTabPagePreloading.sys.mjs @@ -182,6 +182,24 @@ export let NewTabPagePreloading = { this.browserCounts[countKey]--; browser.removeAttribute("preloadedState"); browser.setAttribute("autocompletepopup", "PopupAutoComplete"); + // Let a preloaded about:tor page know that it is no longer preloaded + // (about to be shown). See tor-browser#44314. + // NOTE: We call the AboutTorParent instance directly because it is not + // reliable for the AboutTorParent to wait for the "preloadedState" + // attribute to change via a MutationObserver on the browsingContext's + // browser element because the AboutTorParent's browsingContext's browser + // element may be swapped out. E.g. see the "SwapDocShells" event. + // NOTE: We assume that this is the only place that removes the + // "preloadedState" attribute. + // NOTE: Alternatively, we could have the AboutTorParent wait for + // MozAfterPaint, but this would be slightly delayed. + try { + browser.browsingContext?.currentWindowGlobal + ?.getActor("AboutTor") + .preloadedRemoved(); + } catch { + // Not an about:tor page with an AboutTorParent instance. + } } return browser; diff --git a/browser/modules/HomePage.sys.mjs b/browser/modules/HomePage.sys.mjs @@ -15,7 +15,7 @@ ChromeUtils.defineESModuleGetters(lazy, { }); const kPrefName = "browser.startup.homepage"; -const kDefaultHomePage = "about:blank"; +const kDefaultHomePage = "about:tor"; const kExtensionControllerPref = "browser.startup.homepage_override.extensionControlled"; const kHomePageIgnoreListId = "homepage-urls";