From c5286924511158c2e134164e75d868720130d8b4 Mon Sep 17 00:00:00 2001 From: Kathy Brade Date: Fri, 24 Aug 2018 14:47:31 -0400 Subject: [PATCH] Bug 26962 - implement new features onboarding (part 1). Add an "Explore" button to the "Circuit Display" panel within new user onboarding which opens the DuckDuckGo .onion and then guides users through a short circuit display tutorial. Allow a few additional UITour actions while limiting as much as possible how it can be used. Tweak the UITour styles to match the Tor Browser branding. All user interface strings are retrieved from Torbutton's browserOnboarding.properties file. --- browser/app/permissions | 4 +- browser/components/uitour/UITour.jsm | 47 ++- browser/components/uitour/UITourChild.jsm | 2 +- browser/extensions/onboarding/bootstrap.js | 16 + .../onboarding/content/Onboarding.jsm | 4 +- .../content/onboarding-tor-circuit-display.js | 283 ++++++++++++++++++ .../content/onboarding-tour-agent.js | 3 - browser/extensions/onboarding/jar.mn | 1 + browser/themes/shared/UITour.inc.css | 30 +- 9 files changed, 362 insertions(+), 28 deletions(-) create mode 100644 browser/extensions/onboarding/content/onboarding-tor-circuit-display.js diff --git a/browser/app/permissions b/browser/app/permissions index 0b7b967cbcbef..fa17e78db211c 100644 --- a/browser/app/permissions +++ b/browser/app/permissions @@ -7,8 +7,8 @@ # See nsPermissionManager.cpp for more... # UITour -origin uitour 1 about:home -origin uitour 1 about:newtab +# DuckDuckGo .onion (used for circuit display onboarding). +origin uitour 1 https://3g2upl4pq6kufc4m.onion origin uitour 1 about:tor # Remote troubleshooting diff --git a/browser/components/uitour/UITour.jsm b/browser/components/uitour/UITour.jsm index 3c3fa00733f78..2a4c81d69374b 100644 --- a/browser/components/uitour/UITour.jsm +++ b/browser/components/uitour/UITour.jsm @@ -76,6 +76,16 @@ const TOR_BROWSER_PAGE_ACTIONS_ALLOWED = new Set([ "torBrowserOpenSecurityLevelPanel", ]); +const TOR_BROWSER_TARGETS_ALLOWED = new Set([ + "torBrowser-circuitDisplay", + "torBrowser-circuitDisplay-diagram", + "torBrowser-circuitDisplay-newCircuitButton", +]); + +const TOR_BROWSER_MENUS_ALLOWED = new Set([ + "controlCenter", +]); + const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([ "forceShowReaderIcon", "getConfiguration", @@ -119,6 +129,14 @@ var UITour = { highlightEffects: ["random", "wobble", "zoom", "color"], targets: new Map([ + ["torBrowser-circuitDisplay", { + query: "#connection-icon", + }], + ["torBrowser-circuitDisplay-diagram", + torBrowserCircuitDisplayTarget("circuit-display-nodes")], + ["torBrowser-circuitDisplay-newCircuitButton", + torBrowserCircuitDisplayTarget("circuit-reload-button")], + [ "accountStatus", { @@ -999,7 +1017,7 @@ var UITour = { // This function is copied to UITourListener. isSafeScheme(aURI) { - let allowedSchemes = new Set(["about"]); + let allowedSchemes = new Set(["about", "https"]); if (!allowedSchemes.has(aURI.scheme)) { log.error("Unsafe scheme:", aURI.scheme); @@ -1045,7 +1063,10 @@ var UITour = { return Promise.reject("Invalid target name specified"); } - let targetObject = this.targets.get(aTargetName); + let targetObject; + if (TOR_BROWSER_TARGETS_ALLOWED.has(aTargetName)) { + targetObject = this.targets.get(aTargetName); + } if (!targetObject) { log.warn( "getTarget: The specified target name is not in the allowed set" @@ -1516,6 +1537,10 @@ var UITour = { }, showMenu(aWindow, aMenuName, aOpenCallback = null) { + if (!TOR_BROWSER_MENUS_ALLOWED.has(aMenuName)) { + return; + } + log.debug("showMenu:", aMenuName); function openMenuButton(aMenuBtn) { if (!aMenuBtn || !aMenuBtn.hasMenu() || aMenuBtn.open) { @@ -1628,6 +1653,10 @@ var UITour = { }, hideMenu(aWindow, aMenuName) { + if (!TOR_BROWSER_MENUS_ALLOWED.has(aMenuName)) { + return; + } + log.debug("hideMenu:", aMenuName); function closeMenuButton(aMenuBtn) { if (aMenuBtn && aMenuBtn.hasMenu()) { @@ -2071,6 +2100,20 @@ function controlCenterTrackingToggleTarget(aUnblock) { }; } +function torBrowserCircuitDisplayTarget(aElemID) { + return { + infoPanelPosition: "rightcenter topleft", + query(aDocument) { + let popup = aDocument.defaultView.gIdentityHandler._identityPopup; + if (popup.state != "open") { + return null; + } + let element = aDocument.getElementById(aElemID); + return UITour.isElementVisible(element) ? element : null; + }, + }; +} + this.UITour.init(); /** diff --git a/browser/components/uitour/UITourChild.jsm b/browser/components/uitour/UITourChild.jsm index 941f4ea71ce3b..d27e1a554e9f9 100644 --- a/browser/components/uitour/UITourChild.jsm +++ b/browser/components/uitour/UITourChild.jsm @@ -31,7 +31,7 @@ class UITourChild extends ActorChild { // This function is copied from UITour.jsm. isSafeScheme(aURI) { - let allowedSchemes = new Set(["about"]); + let allowedSchemes = new Set(["about", "https"]); if (!allowedSchemes.has(aURI.scheme)) { return false; diff --git a/browser/extensions/onboarding/bootstrap.js b/browser/extensions/onboarding/bootstrap.js index 9fa1094d5eea3..830b5c93f3d0c 100644 --- a/browser/extensions/onboarding/bootstrap.js +++ b/browser/extensions/onboarding/bootstrap.js @@ -97,6 +97,19 @@ function setPrefs(prefs) { }); } +function openTorCircuitDisplayPage() { + let kFrameScript = "resource://onboarding/onboarding-tor-circuit-display.js"; + const kOnionURL = "https://3g2upl4pq6kufc4m.onion/"; // DuckDuckGo + let win = Services.wm.getMostRecentWindow('navigator:browser'); + if (win) { + let tabBrowser = win.gBrowser; + let tab = tabBrowser.addTab(kOnionURL); + tabBrowser.selectedTab = tab; + let b = tabBrowser.getBrowserForTab(tab); + b.messageManager.loadFrameScript(kFrameScript, true); + } +} + /** * syncTourChecker listens to and maintains the login status inside, and can be * queried at any time once initialized. @@ -170,6 +183,9 @@ function initContentMessageListener() { isLoggedIn: syncTourChecker.isLoggedIn(), }); break; + case "tor-open-circuit-display-page": + openTorCircuitDisplayPage(); + break; #if 0 // No telemetry in Tor Browser. case "ping-centre": diff --git a/browser/extensions/onboarding/content/Onboarding.jsm b/browser/extensions/onboarding/content/Onboarding.jsm index 1c6529d06b464..aa23deb85c75a 100644 --- a/browser/extensions/onboarding/content/Onboarding.jsm +++ b/browser/extensions/onboarding/content/Onboarding.jsm @@ -137,7 +137,6 @@ var onboardingTourset = { "circuit-display": { id: "onboarding-tour-tor-circuit-display", tourNameId: "onboarding.tour-tor-circuit-display", - instantComplete: true, getPage(win) { let div = win.document.createElement("div"); @@ -921,6 +920,9 @@ class Onboarding { this.gotoNextTourItem(); handledTourActionClick = true; break; + case "onboarding-tour-tor-circuit-display-button": + this.sendMessageToChrome("tor-open-circuit-display-page"); + break; } if (classList.contains("onboarding-tour-item")) { this.telemetry({ diff --git a/browser/extensions/onboarding/content/onboarding-tor-circuit-display.js b/browser/extensions/onboarding/content/onboarding-tor-circuit-display.js new file mode 100644 index 0000000000000..de4b23c84c2a4 --- /dev/null +++ b/browser/extensions/onboarding/content/onboarding-tor-circuit-display.js @@ -0,0 +1,283 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// vim: set sw=2 sts=2 ts=8 et syntax=javascript: + +let gStringBundle; + +let domLoadedListener = (aEvent) => { + let doc = aEvent.originalTarget; + if (doc.nodeName == "#document") { + removeEventListener("DOMContentLoaded", domLoadedListener); + beginCircuitDisplayOnboarding(); + } +}; + +addEventListener("DOMContentLoaded", domLoadedListener, false); + +function beginCircuitDisplayOnboarding() { + // 1 of 3: Show the introductory "How do circuits work?" info panel. + let target = "torBrowser-circuitDisplay"; + let title = getStringFromName("intro.title"); + let msg = getStringFromName("intro.msg"); + let button1Label = getStringFromName("one-of-three"); + let button2Label = getStringFromName("next"); + let buttons = []; + buttons.push({label: button1Label, style: "text"}); + buttons.push({label: button2Label, style: "primary", callback: function() { + showCircuitDiagram(); }}); + let options = {closeButtonCallback: function() { cleanUp(); }}; + Mozilla.UITour.showInfo(target, title, msg, undefined, buttons, options); +} + +function showCircuitDiagram() { + // 2 of 3: Open the control center and show the circuit diagram info panel. + Mozilla.UITour.showMenu("controlCenter", function() { + let target = "torBrowser-circuitDisplay-diagram"; + let title = getStringFromName("diagram.title"); + let msg = getStringFromName("diagram.msg"); + let button1Label = getStringFromName("two-of-three"); + let button2Label = getStringFromName("next"); + let buttons = []; + buttons.push({label: button1Label, style: "text"}); + buttons.push({label: button2Label, style: "primary", callback: function() { + showNewCircuitButton(); }}); + let options = {closeButtonCallback: function() { cleanUp(); }}; + Mozilla.UITour.showInfo(target, title, msg, undefined, buttons, options); + }); +} + +function showNewCircuitButton() { + // 3 of 3: Show the New Circuit button info panel. + let target = "torBrowser-circuitDisplay-newCircuitButton"; + let title = getStringFromName("new-circuit.title"); + let msg = getStringFromName("new-circuit.msg"); + let button1Label = getStringFromName("three-of-three"); + let button2Label = getStringFromName("done"); + let buttons = []; + buttons.push({label: button1Label, style: "text"}); + buttons.push({label: button2Label, style: "primary", callback: function() { + cleanUp(); }}); + let options = {closeButtonCallback: function() { cleanUp(); }}; + Mozilla.UITour.showInfo(target, title, msg, undefined, buttons, options); +} + +function cleanUp() { + Mozilla.UITour.hideMenu("controlCenter"); + Mozilla.UITour.closeTab(); +} + +function getStringFromName(aName) { + const TORBUTTON_BUNDLE_URI = "chrome://torbutton/locale/browserOnboarding.properties"; + const PREFIX = "onboarding.tor-circuit-display."; + + if (!gStringBundle) { + gStringBundle = Services.strings.createBundle(TORBUTTON_BUNDLE_URI) + } + + let result; + try { + result = gStringBundle.GetStringFromName(PREFIX + aName); + } catch (e) { + result = aName; + } + return result; +} + + +// The remainder of the code in this file was adapted from +// browser/components/uitour/UITour-lib.js (unfortunately, we cannot use that +// code here because it directly accesses 'document' and it assumes that the +// content window is the global JavaScript object), + +/* 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/. */ + +// create namespace +if (typeof Mozilla == "undefined") { + var Mozilla = {}; +} + +(function($) { + "use strict"; + + // create namespace + if (typeof Mozilla.UITour == "undefined") { + /** + * Library that exposes an event-based Web API for communicating with the + * desktop browser chrome. It can be used for tasks such as opening menu + * panels and highlighting the position of buttons in the toolbar. + * + *

For security/privacy reasons `Mozilla.UITour` will only work on a list of allowed + * secure origins. The list of allowed origins can be found in + * {@link https://dxr.mozilla.org/mozilla-central/source/browser/app/permissions| + * browser/app/permissions}.

+ * + * @since 29 + * @namespace + */ + Mozilla.UITour = {}; + } + + function _sendEvent(action, data) { + var event = new content.CustomEvent("mozUITour", { + bubbles: true, + detail: { + action, + data: data || {} + } + }); + + content.document.dispatchEvent(event); + } + + function _generateCallbackID() { + return Math.random().toString(36).replace(/[^a-z]+/g, ""); + } + + function _waitForCallback(callback) { + var id = _generateCallbackID(); + + function listener(event) { + if (typeof event.detail != "object") + return; + if (event.detail.callbackID != id) + return; + + content.document.removeEventListener("mozUITourResponse", listener); + callback(event.detail.data); + } + content.document.addEventListener("mozUITourResponse", listener); + + return id; + } + + /** + * Show an arrow panel with optional images and buttons anchored at a specific UI target. + * + * @see Mozilla.UITour.hideInfo + * + * @param {Mozilla.UITour.Target} target - Identifier of the UI widget to anchor the panel at. + * @param {String} title - Title text to be shown as the heading of the panel. + * @param {String} text - Body text of the panel. + * @param {String} [icon=null] - URL of a 48x48px (96px @ 2dppx) image (which will be resolved + * relative to the tab's URI) to display in the panel. + * @param {Object[]} [buttons=[]] - Array of objects describing buttons. + * @param {String} buttons[].label - Button label + * @param {String} buttons[].icon - Button icon URL + * @param {String} buttons[].style - Button style ("primary" or "link") + * @param {Function} buttons[].callback - Called when the button is clicked + * @param {Object} [options={}] - Advanced options + * @param {Function} options.closeButtonCallback - Called when the panel's close button is clicked. + * + * @example + * var buttons = [ + * { + * label: 'Cancel', + * style: 'link', + * callback: cancelBtnCallback + * }, + * { + * label: 'Confirm', + * style: 'primary', + * callback: confirmBtnCallback + * } + * ]; + * + * var icon = '//mozorg.cdn.mozilla.net/media/img/firefox/australis/logo.png'; + * + * var options = { + * closeButtonCallback: closeBtnCallback + * }; + * + * Mozilla.UITour.showInfo('appMenu', 'my title', 'my text', icon, buttons, options); + */ + Mozilla.UITour.showInfo = function(target, title, text, icon, buttons, options) { + var buttonData = []; + if (Array.isArray(buttons)) { + for (var i = 0; i < buttons.length; i++) { + buttonData.push({ + label: buttons[i].label, + icon: buttons[i].icon, + style: buttons[i].style, + callbackID: _waitForCallback(buttons[i].callback) + }); + } + } + + var closeButtonCallbackID, targetCallbackID; + if (options && options.closeButtonCallback) + closeButtonCallbackID = _waitForCallback(options.closeButtonCallback); + if (options && options.targetCallback) + targetCallbackID = _waitForCallback(options.targetCallback); + + _sendEvent("showInfo", { + target, + title, + text, + icon, + buttons: buttonData, + closeButtonCallbackID, + targetCallbackID + }); + }; + + /** + * Hide any visible info panels. + * @see Mozilla.UITour.showInfo + */ + Mozilla.UITour.hideInfo = function() { + _sendEvent("hideInfo"); + }; + + /** + * Open the named application menu. + * + * @see Mozilla.UITour.hideMenu + * + * @param {Mozilla.UITour.MenuName} name - Menu name + * @param {Function} [callback] - Callback to be called with no arguments when + * the menu opens. + * + * @example + * Mozilla.UITour.showMenu('appMenu', function() { + * console.log('menu was opened'); + * }); + */ + Mozilla.UITour.showMenu = function(name, callback) { + var showCallbackID; + if (callback) + showCallbackID = _waitForCallback(callback); + + _sendEvent("showMenu", { + name, + showCallbackID, + }); + }; + + /** + * Close the named application menu. + * + * @see Mozilla.UITour.showMenu + * + * @param {Mozilla.UITour.MenuName} name - Menu name + */ + Mozilla.UITour.hideMenu = function(name) { + _sendEvent("hideMenu", { + name + }); + }; + + /** + * @summary Closes the tab where this code is running. As usual, if the tab is in the + * foreground, the tab that was displayed before is selected. + * + * @description The last tab in the current window will never be closed, in which case + * this call will have no effect. The calling code is expected to take an + * action after a small timeout in order to handle this case, for example by + * displaying a goodbye message or a button to restart the tour. + * @since 46 + */ + Mozilla.UITour.closeTab = function() { + _sendEvent("closeTab"); + }; +})(); diff --git a/browser/extensions/onboarding/content/onboarding-tour-agent.js b/browser/extensions/onboarding/content/onboarding-tour-agent.js index bb4555cbc2a3d..a08320d0535af 100644 --- a/browser/extensions/onboarding/content/onboarding-tour-agent.js +++ b/browser/extensions/onboarding/content/onboarding-tour-agent.js @@ -18,9 +18,6 @@ let onCanSetDefaultBrowserInBackground = () => { let onClick = evt => { switch (evt.target.id) { - case "onboarding-tour-tor-circuit-display-button": - // TODO: open circuit display onboarding - break; case "onboarding-tour-tor-security-button": Mozilla.UITour.torBrowserOpenSecurityLevelPanel(); break; diff --git a/browser/extensions/onboarding/jar.mn b/browser/extensions/onboarding/jar.mn index cbf62b2242a93..29a24ad695d15 100644 --- a/browser/extensions/onboarding/jar.mn +++ b/browser/extensions/onboarding/jar.mn @@ -10,6 +10,7 @@ content/img/ (content/img/*) * content/onboarding-tour-agent.js (content/onboarding-tour-agent.js) * content/onboarding.js (content/onboarding.js) + content/onboarding-tor-circuit-display.js (content/onboarding-tor-circuit-display.js) # Package UITour-lib.js in here rather than under # /browser/components/uitour to avoid "unreferenced files" error when # Onboarding extension is not built. diff --git a/browser/themes/shared/UITour.inc.css b/browser/themes/shared/UITour.inc.css index 8c2d955f5622b..235e4d898e783 100644 --- a/browser/themes/shared/UITour.inc.css +++ b/browser/themes/shared/UITour.inc.css @@ -49,7 +49,8 @@ } #UITourTooltipTitle { - font-size: 1.45rem; + color: #420C5D; + font-size: 16px; font-weight: bold; margin: 0; } @@ -57,7 +58,8 @@ #UITourTooltipDescription { margin-inline-start: 0; margin-inline-end: 0; - font-size: 1.15rem; + color: #4A4A4A; + font-size: 13px; line-height: 1.8rem; margin-bottom: 0; /* Override global.css */ } @@ -79,7 +81,6 @@ #UITourTooltipButtons { -moz-box-pack: end; background-color: var(--arrowpanel-dimmed); - border-top: 1px solid var(--panel-separator-color); margin: 10px -16px -16px; padding: 16px; } @@ -113,40 +114,31 @@ #UITourTooltipButtons > button:not(.button-link) { -moz-appearance: none; background-color: rgb(251,251,251); - border-radius: 3px; - border: 1px solid; - border-color: rgb(192,192,192); + border-radius: 2px; color: rgb(71,71,71); - padding: 4px 30px; + padding: 6px 30px; transition-property: background-color, border-color; transition-duration: 150ms; } -#UITourTooltipButtons > button:not(.button-link):not(:active):hover { - background-color: hsla(210,4%,10%,.15); - border-color: hsla(210,4%,10%,.15); - box-shadow: 0 1px 0 0 hsla(210,4%,10%,.05) inset; -} - #UITourTooltipButtons > label, #UITourTooltipButtons > button.button-link:not(:hover) { -moz-appearance: none; background: transparent; border: none; box-shadow: none; - color: var(--panel-disabled-color); + color: #4A4A4A; padding-left: 10px; padding-right: 10px; } -/* The primary button gets the same color as the customize button. */ #UITourTooltipButtons > button.button-primary { - background-color: rgb(116,191,67); + background-color: #420C5D; color: white; - padding-left: 30px; - padding-right: 30px; + padding-left: 28px; + padding-right: 28px; } #UITourTooltipButtons > button.button-primary:not(:active):hover { - background-color: rgb(105,173,61); + background-color: #410A4E; } -- GitLab