diff options
Diffstat (limited to 'browser/components/uitour/UITour.jsm')
-rw-r--r-- | browser/components/uitour/UITour.jsm | 2111 |
1 files changed, 0 insertions, 2111 deletions
diff --git a/browser/components/uitour/UITour.jsm b/browser/components/uitour/UITour.jsm deleted file mode 100644 index b92715963..000000000 --- a/browser/components/uitour/UITour.jsm +++ /dev/null @@ -1,2111 +0,0 @@ -// 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/. - -"use strict"; - -this.EXPORTED_SYMBOLS = ["UITour"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/AppConstants.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource:///modules/RecentWindow.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/TelemetryController.jsm"); -Cu.import("resource://gre/modules/Timer.jsm"); - -Cu.importGlobalProperties(["URL"]); - -XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", - "resource://gre/modules/LightweightThemeManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ResetProfile", - "resource://gre/modules/ResetProfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", - "resource:///modules/CustomizableUI.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", - "resource://gre/modules/UITelemetry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", - "resource:///modules/BrowserUITelemetry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", - "resource://gre/modules/ReaderMode.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent", - "resource:///modules/ReaderParent.jsm"); - -// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error". -const PREF_LOG_LEVEL = "browser.uitour.loglevel"; -const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs"; -const PREF_READERVIEW_TRIGGER = "browser.uitour.readerViewTrigger"; -const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration"; - -const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([ - "forceShowReaderIcon", - "getConfiguration", - "getTreatmentTag", - "hideHighlight", - "hideInfo", - "hideMenu", - "ping", - "registerPageID", - "setConfiguration", - "setTreatmentTag", -]); -const MAX_BUTTONS = 4; - -const BUCKET_NAME = "UITour"; -const BUCKET_TIMESTEPS = [ - 1 * 60 * 1000, // Until 1 minute after tab is closed/inactive. - 3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive. - 10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive. - 60 * 60 * 1000, // Until 1 hour after tab is closed/inactive. -]; - -// Time after which seen Page IDs expire. -const SEENPAGEID_EXPIRY = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks. - -// Prefix for any target matching a search engine. -const TARGET_SEARCHENGINE_PREFIX = "searchEngine-"; - -// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref. -XPCOMUtils.defineLazyGetter(this, "log", () => { - let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI; - let consoleOptions = { - maxLogLevelPref: PREF_LOG_LEVEL, - prefix: "UITour", - }; - return new ConsoleAPI(consoleOptions); -}); - -this.UITour = { - url: null, - seenPageIDs: null, - // This map is not persisted and is used for - // building the content source of a potential tour. - pageIDsForSession: new Map(), - pageIDSourceBrowsers: new WeakMap(), - /* Map from browser chrome windows to a Set of <browser>s in which a tour is open (both visible and hidden) */ - tourBrowsersByWindow: new WeakMap(), - appMenuOpenForAnnotation: new Set(), - availableTargetsCache: new WeakMap(), - clearAvailableTargetsCache() { - this.availableTargetsCache = new WeakMap(); - }, - - _annotationPanelMutationObservers: new WeakMap(), - - highlightEffects: ["random", "wobble", "zoom", "color"], - targets: new Map([ - ["accountStatus", { - query: (aDocument) => { - // If the user is logged in, use the avatar element. - let fxAFooter = aDocument.getElementById("PanelUI-footer-fxa"); - if (fxAFooter.getAttribute("fxastatus")) { - return aDocument.getElementById("PanelUI-fxa-avatar"); - } - - // Otherwise use the sync setup icon. - let statusButton = aDocument.getElementById("PanelUI-fxa-label"); - return aDocument.getAnonymousElementByAttribute(statusButton, - "class", - "toolbarbutton-icon"); - }, - // This is a fake widgetName starting with the "PanelUI-" prefix so we know - // to automatically open the appMenu when annotating this target. - widgetName: "PanelUI-fxa-label", - }], - ["addons", {query: "#add-ons-button"}], - ["appMenu", { - addTargetListener: (aDocument, aCallback) => { - let panelPopup = aDocument.getElementById("PanelUI-popup"); - panelPopup.addEventListener("popupshown", aCallback); - }, - query: "#PanelUI-button", - removeTargetListener: (aDocument, aCallback) => { - let panelPopup = aDocument.getElementById("PanelUI-popup"); - panelPopup.removeEventListener("popupshown", aCallback); - }, - }], - ["backForward", { - query: "#back-button", - widgetName: "urlbar-container", - }], - ["bookmarks", {query: "#bookmarks-menu-button"}], - ["controlCenter-trackingUnblock", controlCenterTrackingToggleTarget(true)], - ["controlCenter-trackingBlock", controlCenterTrackingToggleTarget(false)], - ["customize", { - query: (aDocument) => { - let customizeButton = aDocument.getElementById("PanelUI-customize"); - return aDocument.getAnonymousElementByAttribute(customizeButton, - "class", - "toolbarbutton-icon"); - }, - widgetName: "PanelUI-customize", - }], - ["devtools", {query: "#developer-button"}], - ["help", {query: "#PanelUI-help"}], - ["home", {query: "#home-button"}], - ["forget", { - allowAdd: true, - query: "#panic-button", - widgetName: "panic-button", - }], - ["pocket", { - allowAdd: true, - query: "#pocket-button", - widgetName: "pocket-button", - }], - ["privateWindow", {query: "#privatebrowsing-button"}], - ["quit", {query: "#PanelUI-quit"}], - ["readerMode-urlBar", {query: "#reader-mode-button"}], - ["search", { - infoPanelOffsetX: 18, - infoPanelPosition: "after_start", - query: "#searchbar", - widgetName: "search-container", - }], - ["searchIcon", { - query: (aDocument) => { - let searchbar = aDocument.getElementById("searchbar"); - return aDocument.getAnonymousElementByAttribute(searchbar, - "anonid", - "searchbar-search-button"); - }, - widgetName: "search-container", - }], - ["searchPrefsLink", { - query: (aDocument) => { - let element = null; - let popup = aDocument.getElementById("PopupSearchAutoComplete"); - if (popup.state != "open") - return null; - element = aDocument.getAnonymousElementByAttribute(popup, - "anonid", - "search-settings"); - if (!element || !UITour.isElementVisible(element)) { - return null; - } - return element; - }, - }], - ["selectedTabIcon", { - query: (aDocument) => { - let selectedtab = aDocument.defaultView.gBrowser.selectedTab; - let element = aDocument.getAnonymousElementByAttribute(selectedtab, - "anonid", - "tab-icon-image"); - if (!element || !UITour.isElementVisible(element)) { - return null; - } - return element; - }, - }], - ["trackingProtection", { - query: "#tracking-protection-icon", - }], - ["urlbar", { - query: "#urlbar", - widgetName: "urlbar-container", - }], - ["webide", {query: "#webide-button"}], - ]), - - init: function() { - log.debug("Initializing UITour"); - // Lazy getter is initialized here so it can be replicated any time - // in a test. - delete this.seenPageIDs; - Object.defineProperty(this, "seenPageIDs", { - get: this.restoreSeenPageIDs.bind(this), - configurable: true, - }); - - delete this.url; - XPCOMUtils.defineLazyGetter(this, "url", function () { - return Services.urlFormatter.formatURLPref("browser.uitour.url"); - }); - - // Clear the availableTargetsCache on widget changes. - let listenerMethods = [ - "onWidgetAdded", - "onWidgetMoved", - "onWidgetRemoved", - "onWidgetReset", - "onAreaReset", - ]; - CustomizableUI.addListener(listenerMethods.reduce((listener, method) => { - listener[method] = () => this.clearAvailableTargetsCache(); - return listener; - }, {})); - }, - - restoreSeenPageIDs: function() { - delete this.seenPageIDs; - - if (UITelemetry.enabled) { - let dateThreshold = Date.now() - SEENPAGEID_EXPIRY; - - try { - let data = Services.prefs.getCharPref(PREF_SEENPAGEIDS); - data = new Map(JSON.parse(data)); - - for (let [pageID, details] of data) { - - if (typeof pageID != "string" || - typeof details != "object" || - typeof details.lastSeen != "number" || - details.lastSeen < dateThreshold) { - - data.delete(pageID); - } - } - - this.seenPageIDs = data; - } catch (e) {} - } - - if (!this.seenPageIDs) - this.seenPageIDs = new Map(); - - this.persistSeenIDs(); - - return this.seenPageIDs; - }, - - addSeenPageID: function(aPageID) { - if (!UITelemetry.enabled) - return; - - this.seenPageIDs.set(aPageID, { - lastSeen: Date.now(), - }); - - this.persistSeenIDs(); - }, - - persistSeenIDs: function() { - if (this.seenPageIDs.size === 0) { - Services.prefs.clearUserPref(PREF_SEENPAGEIDS); - return; - } - - Services.prefs.setCharPref(PREF_SEENPAGEIDS, - JSON.stringify([...this.seenPageIDs])); - }, - - get _readerViewTriggerRegEx() { - delete this._readerViewTriggerRegEx; - let readerViewUITourTrigger = Services.prefs.getCharPref(PREF_READERVIEW_TRIGGER); - return this._readerViewTriggerRegEx = new RegExp(readerViewUITourTrigger, "i"); - }, - - onLocationChange: function(aLocation) { - // The ReaderView tour page is expected to run in Reader View, - // which disables JavaScript on the page. To get around that, we - // automatically start a pre-defined tour on page load (for hysterical - // raisins the ReaderView tour is known as "readinglist") - let originalUrl = ReaderMode.getOriginalUrl(aLocation); - if (this._readerViewTriggerRegEx.test(originalUrl)) { - this.startSubTour("readinglist"); - } - }, - - onPageEvent: function(aMessage, aEvent) { - let browser = aMessage.target; - let window = browser.ownerGlobal; - - // Does the window have tabs? We need to make sure since windowless browsers do - // not have tabs. - if (!window.gBrowser) { - // When using windowless browsers we don't have a valid |window|. If that's the case, - // use the most recent window as a target for UITour functions (see Bug 1111022). - window = Services.wm.getMostRecentWindow("navigator:browser"); - } - - let messageManager = browser.messageManager; - - log.debug("onPageEvent:", aEvent.detail, aMessage); - - if (typeof aEvent.detail != "object") { - log.warn("Malformed event - detail not an object"); - return false; - } - - let action = aEvent.detail.action; - if (typeof action != "string" || !action) { - log.warn("Action not defined"); - return false; - } - - let data = aEvent.detail.data; - if (typeof data != "object") { - log.warn("Malformed event - data not an object"); - return false; - } - - if ((aEvent.pageVisibilityState == "hidden" || - aEvent.pageVisibilityState == "unloaded") && - !BACKGROUND_PAGE_ACTIONS_ALLOWED.has(action)) { - log.warn("Ignoring disallowed action from a hidden page:", action); - return false; - } - - switch (action) { - case "registerPageID": { - if (typeof data.pageID != "string") { - log.warn("registerPageID: pageID must be a string"); - break; - } - - this.pageIDsForSession.set(data.pageID, {lastSeen: Date.now()}); - - // The rest is only relevant if Telemetry is enabled. - if (!UITelemetry.enabled) { - log.debug("registerPageID: Telemetry disabled, not doing anything"); - break; - } - - // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the - // pageID, as it could make parsing the telemetry bucket name difficult. - if (data.pageID.includes(BrowserUITelemetry.BUCKET_SEPARATOR)) { - log.warn("registerPageID: Invalid page ID specified"); - break; - } - - this.addSeenPageID(data.pageID); - this.pageIDSourceBrowsers.set(browser, data.pageID); - this.setTelemetryBucket(data.pageID); - - break; - } - - case "showHeartbeat": { - // Validate the input parameters. - if (typeof data.message !== "string" || data.message === "") { - log.error("showHeartbeat: Invalid message specified."); - return false; - } - - if (typeof data.thankyouMessage !== "string" || data.thankyouMessage === "") { - log.error("showHeartbeat: Invalid thank you message specified."); - return false; - } - - if (typeof data.flowId !== "string" || data.flowId === "") { - log.error("showHeartbeat: Invalid flowId specified."); - return false; - } - - if (data.engagementButtonLabel && typeof data.engagementButtonLabel != "string") { - log.error("showHeartbeat: Invalid engagementButtonLabel specified"); - return false; - } - - let heartbeatWindow = window; - if (data.privateWindowsOnly && !PrivateBrowsingUtils.isWindowPrivate(heartbeatWindow)) { - heartbeatWindow = RecentWindow.getMostRecentBrowserWindow({ private: true }); - if (!heartbeatWindow) { - log.debug("showHeartbeat: No private window found"); - return false; - } - } - - // Finally show the Heartbeat UI. - this.showHeartbeat(heartbeatWindow, data); - break; - } - - case "showHighlight": { - let targetPromise = this.getTarget(window, data.target); - targetPromise.then(target => { - if (!target.node) { - log.error("UITour: Target could not be resolved: " + data.target); - return; - } - let effect = undefined; - if (this.highlightEffects.indexOf(data.effect) !== -1) { - effect = data.effect; - } - this.showHighlight(window, target, effect); - }).catch(log.error); - break; - } - - case "hideHighlight": { - this.hideHighlight(window); - break; - } - - case "showInfo": { - let targetPromise = this.getTarget(window, data.target, true); - targetPromise.then(target => { - if (!target.node) { - log.error("UITour: Target could not be resolved: " + data.target); - return; - } - - let iconURL = null; - if (typeof data.icon == "string") - iconURL = this.resolveURL(browser, data.icon); - - let buttons = []; - if (Array.isArray(data.buttons) && data.buttons.length > 0) { - for (let buttonData of data.buttons) { - if (typeof buttonData == "object" && - typeof buttonData.label == "string" && - typeof buttonData.callbackID == "string") { - let callback = buttonData.callbackID; - let button = { - label: buttonData.label, - callback: event => { - this.sendPageCallback(messageManager, callback); - }, - }; - - if (typeof buttonData.icon == "string") - button.iconURL = this.resolveURL(browser, buttonData.icon); - - if (typeof buttonData.style == "string") - button.style = buttonData.style; - - buttons.push(button); - - if (buttons.length == MAX_BUTTONS) { - log.warn("showInfo: Reached limit of allowed number of buttons"); - break; - } - } - } - } - - let infoOptions = {}; - if (typeof data.closeButtonCallbackID == "string") { - infoOptions.closeButtonCallback = () => { - this.sendPageCallback(messageManager, data.closeButtonCallbackID); - }; - } - if (typeof data.targetCallbackID == "string") { - infoOptions.targetCallback = details => { - this.sendPageCallback(messageManager, data.targetCallbackID, details); - }; - } - - this.showInfo(window, target, data.title, data.text, iconURL, buttons, infoOptions); - }).catch(log.error); - break; - } - - case "hideInfo": { - this.hideInfo(window); - break; - } - - case "previewTheme": { - this.previewTheme(data.theme); - break; - } - - case "resetTheme": { - this.resetTheme(); - break; - } - - case "showMenu": { - this.showMenu(window, data.name, () => { - if (typeof data.showCallbackID == "string") - this.sendPageCallback(messageManager, data.showCallbackID); - }); - break; - } - - case "hideMenu": { - this.hideMenu(window, data.name); - break; - } - - case "showNewTab": { - this.showNewTab(window, browser); - break; - } - - case "getConfiguration": { - if (typeof data.configuration != "string") { - log.warn("getConfiguration: No configuration option specified"); - return false; - } - - this.getConfiguration(messageManager, window, data.configuration, data.callbackID); - break; - } - - case "setConfiguration": { - if (typeof data.configuration != "string") { - log.warn("setConfiguration: No configuration option specified"); - return false; - } - - this.setConfiguration(window, data.configuration, data.value); - break; - } - - case "openPreferences": { - if (typeof data.pane != "string" && typeof data.pane != "undefined") { - log.warn("openPreferences: Invalid pane specified"); - return false; - } - - window.openPreferences(data.pane); - break; - } - - case "showFirefoxAccounts": { - // 'signup' is the only action that makes sense currently, so we don't - // accept arbitrary actions just to be safe... - let p = new URLSearchParams("action=signup&entrypoint=uitour"); - // Call our helper to validate extraURLCampaignParams and populate URLSearchParams - if (!this._populateCampaignParams(p, data.extraURLCampaignParams)) { - log.warn("showFirefoxAccounts: invalid campaign args specified"); - return false; - } - - // We want to replace the current tab. - browser.loadURI("about:accounts?" + p.toString()); - break; - } - - case "resetFirefox": { - // Open a reset profile dialog window. - if (ResetProfile.resetSupported()) { - ResetProfile.openConfirmationDialog(window); - } - break; - } - - case "addNavBarWidget": { - // Add a widget to the toolbar - let targetPromise = this.getTarget(window, data.name); - targetPromise.then(target => { - this.addNavBarWidget(target, messageManager, data.callbackID); - }).catch(log.error); - break; - } - - case "setDefaultSearchEngine": { - let enginePromise = this.selectSearchEngine(data.identifier); - enginePromise.catch(Cu.reportError); - break; - } - - case "setTreatmentTag": { - let name = data.name; - let value = data.value; - let string = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); - string.data = value; - Services.prefs.setComplexValue("browser.uitour.treatment." + name, - Ci.nsISupportsString, string); - // The notification is only meant to be used in tests. - UITourHealthReport.recordTreatmentTag(name, value) - .then(() => this.notify("TreatmentTag:TelemetrySent")); - break; - } - - case "getTreatmentTag": { - let name = data.name; - let value; - try { - value = Services.prefs.getComplexValue("browser.uitour.treatment." + name, - Ci.nsISupportsString).data; - } catch (ex) {} - this.sendPageCallback(messageManager, data.callbackID, { value: value }); - break; - } - - case "setSearchTerm": { - let targetPromise = this.getTarget(window, "search"); - targetPromise.then(target => { - let searchbar = target.node; - searchbar.value = data.term; - searchbar.updateGoButtonVisibility(); - }); - break; - } - - case "openSearchPanel": { - let targetPromise = this.getTarget(window, "search"); - targetPromise.then(target => { - let searchbar = target.node; - - if (searchbar.textbox.open) { - this.sendPageCallback(messageManager, data.callbackID); - } else { - let onPopupShown = () => { - searchbar.textbox.popup.removeEventListener("popupshown", onPopupShown); - this.sendPageCallback(messageManager, data.callbackID); - }; - - searchbar.textbox.popup.addEventListener("popupshown", onPopupShown); - searchbar.openSuggestionsPanel(); - } - }).then(null, Cu.reportError); - break; - } - - case "ping": { - if (typeof data.callbackID == "string") - this.sendPageCallback(messageManager, data.callbackID); - break; - } - - case "forceShowReaderIcon": { - ReaderParent.forceShowReaderIcon(browser); - break; - } - - case "toggleReaderMode": { - let targetPromise = this.getTarget(window, "readerMode-urlBar"); - targetPromise.then(target => { - ReaderParent.toggleReaderMode({target: target.node}); - }); - break; - } - - case "closeTab": { - // Find the <tabbrowser> element of the <browser> for which the event - // was generated originally. If the browser where the UI tour is loaded - // is windowless, just ignore the request to close the tab. The request - // is also ignored if this is the only tab in the window. - let tabBrowser = browser.ownerGlobal.gBrowser; - if (tabBrowser && tabBrowser.browsers.length > 1) { - tabBrowser.removeTab(tabBrowser.getTabForBrowser(browser)); - } - break; - } - } - - this.initForBrowser(browser, window); - - return true; - }, - - initForBrowser(aBrowser, window) { - let gBrowser = window.gBrowser; - - if (gBrowser) { - gBrowser.tabContainer.addEventListener("TabSelect", this); - } - - if (!this.tourBrowsersByWindow.has(window)) { - this.tourBrowsersByWindow.set(window, new Set()); - } - this.tourBrowsersByWindow.get(window).add(aBrowser); - - Services.obs.addObserver(this, "message-manager-close", false); - - window.addEventListener("SSWindowClosing", this); - }, - - handleEvent: function(aEvent) { - log.debug("handleEvent: type =", aEvent.type, "event =", aEvent); - switch (aEvent.type) { - case "TabSelect": { - let window = aEvent.target.ownerGlobal; - - // Teardown the browser of the tab we just switched away from. - if (aEvent.detail && aEvent.detail.previousTab) { - let previousTab = aEvent.detail.previousTab; - let openTourWindows = this.tourBrowsersByWindow.get(window); - if (openTourWindows.has(previousTab.linkedBrowser)) { - this.teardownTourForBrowser(window, previousTab.linkedBrowser, false); - } - } - - break; - } - - case "SSWindowClosing": { - let window = aEvent.target; - this.teardownTourForWindow(window); - break; - } - } - }, - - observe: function(aSubject, aTopic, aData) { - log.debug("observe: aTopic =", aTopic); - switch (aTopic) { - // The browser message manager is disconnected when the <browser> is - // destroyed and we want to teardown at that point. - case "message-manager-close": { - let winEnum = Services.wm.getEnumerator("navigator:browser"); - while (winEnum.hasMoreElements()) { - let window = winEnum.getNext(); - if (window.closed) - continue; - - let tourBrowsers = this.tourBrowsersByWindow.get(window); - if (!tourBrowsers) - continue; - - for (let browser of tourBrowsers) { - let messageManager = browser.messageManager; - if (aSubject != messageManager) { - continue; - } - - this.teardownTourForBrowser(window, browser, true); - return; - } - } - break; - } - } - }, - - // Given a string that is a JSONified represenation of an object with - // additional utm_* URL params that should be appended, validate and append - // them to the passed URLSearchParams object. Returns true if the params - // were validated and appended, and false if the request should be ignored. - _populateCampaignParams: function(urlSearchParams, extraURLCampaignParams) { - // We are extra paranoid about what params we allow to be appended. - if (typeof extraURLCampaignParams == "undefined") { - // no params, so it's all good. - return true; - } - if (typeof extraURLCampaignParams != "string") { - log.warn("_populateCampaignParams: extraURLCampaignParams is not a string"); - return false; - } - let campaignParams; - try { - if (extraURLCampaignParams) { - campaignParams = JSON.parse(extraURLCampaignParams); - if (typeof campaignParams != "object") { - log.warn("_populateCampaignParams: extraURLCampaignParams is not a stringified object"); - return false; - } - } - } catch (ex) { - log.warn("_populateCampaignParams: extraURLCampaignParams is not a JSON object"); - return false; - } - if (campaignParams) { - // The regex that the name of each param must match - there's no - // character restriction on the value - they will be escaped as necessary. - let reSimpleString = /^[-_a-zA-Z0-9]*$/; - for (let name in campaignParams) { - let value = campaignParams[name]; - if (typeof name != "string" || typeof value != "string" || - !name.startsWith("utm_") || - value.length == 0 || - !reSimpleString.test(name)) { - log.warn("_populateCampaignParams: invalid campaign param specified"); - return false; - } - urlSearchParams.append(name, value); - } - } - return true; - }, - - setTelemetryBucket: function(aPageID) { - let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID; - BrowserUITelemetry.setBucket(bucket); - }, - - setExpiringTelemetryBucket: function(aPageID, aType) { - let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID + - BrowserUITelemetry.BUCKET_SEPARATOR + aType; - - BrowserUITelemetry.setExpiringBucket(bucket, - BUCKET_TIMESTEPS); - }, - - // This is registered with UITelemetry by BrowserUITelemetry, so that UITour - // can remain lazy-loaded on-demand. - getTelemetry: function() { - return { - seenPageIDs: [...this.seenPageIDs.keys()], - }; - }, - - /** - * Tear down a tour from a tab e.g. upon switching/closing tabs. - */ - teardownTourForBrowser: function(aWindow, aBrowser, aTourPageClosing = false) { - log.debug("teardownTourForBrowser: aBrowser = ", aBrowser, aTourPageClosing); - - if (this.pageIDSourceBrowsers.has(aBrowser)) { - let pageID = this.pageIDSourceBrowsers.get(aBrowser); - this.setExpiringTelemetryBucket(pageID, aTourPageClosing ? "closed" : "inactive"); - } - - let openTourBrowsers = this.tourBrowsersByWindow.get(aWindow); - if (aTourPageClosing && openTourBrowsers) { - openTourBrowsers.delete(aBrowser); - } - - this.hideHighlight(aWindow); - this.hideInfo(aWindow); - // Ensure the menu panel is hidden before calling recreatePopup so popup events occur. - this.hideMenu(aWindow, "appMenu"); - this.hideMenu(aWindow, "controlCenter"); - - // Clean up panel listeners after calling hideMenu above. - aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hideAppMenuAnnotations); - aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hideAppMenuAnnotations); - aWindow.PanelUI.panel.removeEventListener("popuphidden", this.onPanelHidden); - let controlCenterPanel = aWindow.gIdentityHandler._identityPopup; - controlCenterPanel.removeEventListener("popuphidden", this.onPanelHidden); - controlCenterPanel.removeEventListener("popuphiding", this.hideControlCenterAnnotations); - - this.resetTheme(); - - // If there are no more tour tabs left in the window, teardown the tour for the whole window. - if (!openTourBrowsers || openTourBrowsers.size == 0) { - this.teardownTourForWindow(aWindow); - } - }, - - /** - * Tear down all tours for a ChromeWindow. - */ - teardownTourForWindow: function(aWindow) { - log.debug("teardownTourForWindow"); - aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this); - aWindow.removeEventListener("SSWindowClosing", this); - - let openTourBrowsers = this.tourBrowsersByWindow.get(aWindow); - if (openTourBrowsers) { - for (let browser of openTourBrowsers) { - if (this.pageIDSourceBrowsers.has(browser)) { - let pageID = this.pageIDSourceBrowsers.get(browser); - this.setExpiringTelemetryBucket(pageID, "closed"); - } - } - } - - this.tourBrowsersByWindow.delete(aWindow); - }, - - // This function is copied to UITourListener. - isSafeScheme: function(aURI) { - let allowedSchemes = new Set(["https", "about"]); - if (!Services.prefs.getBoolPref("browser.uitour.requireSecure")) - allowedSchemes.add("http"); - - if (!allowedSchemes.has(aURI.scheme)) { - log.error("Unsafe scheme:", aURI.scheme); - return false; - } - - return true; - }, - - resolveURL: function(aBrowser, aURL) { - try { - let uri = Services.io.newURI(aURL, null, aBrowser.currentURI); - - if (!this.isSafeScheme(uri)) - return null; - - return uri.spec; - } catch (e) {} - - return null; - }, - - sendPageCallback: function(aMessageManager, aCallbackID, aData = {}) { - let detail = {data: aData, callbackID: aCallbackID}; - log.debug("sendPageCallback", detail); - aMessageManager.sendAsyncMessage("UITour:SendPageCallback", detail); - }, - - isElementVisible: function(aElement) { - let targetStyle = aElement.ownerGlobal.getComputedStyle(aElement); - return !aElement.ownerDocument.hidden && - targetStyle.display != "none" && - targetStyle.visibility == "visible"; - }, - - getTarget: function(aWindow, aTargetName, aSticky = false) { - log.debug("getTarget:", aTargetName); - let deferred = Promise.defer(); - if (typeof aTargetName != "string" || !aTargetName) { - log.warn("getTarget: Invalid target name specified"); - deferred.reject("Invalid target name specified"); - return deferred.promise; - } - - let targetObject = this.targets.get(aTargetName); - if (!targetObject) { - log.warn("getTarget: The specified target name is not in the allowed set"); - deferred.reject("The specified target name is not in the allowed set"); - return deferred.promise; - } - - let targetQuery = targetObject.query; - aWindow.PanelUI.ensureReady().then(() => { - let node; - if (typeof targetQuery == "function") { - try { - node = targetQuery(aWindow.document); - } catch (ex) { - log.warn("getTarget: Error running target query:", ex); - node = null; - } - } else { - node = aWindow.document.querySelector(targetQuery); - } - - deferred.resolve({ - addTargetListener: targetObject.addTargetListener, - infoPanelOffsetX: targetObject.infoPanelOffsetX, - infoPanelOffsetY: targetObject.infoPanelOffsetY, - infoPanelPosition: targetObject.infoPanelPosition, - node: node, - removeTargetListener: targetObject.removeTargetListener, - targetName: aTargetName, - widgetName: targetObject.widgetName, - allowAdd: targetObject.allowAdd, - }); - }).catch(log.error); - return deferred.promise; - }, - - targetIsInAppMenu: function(aTarget) { - let placement = CustomizableUI.getPlacementOfWidget(aTarget.widgetName || aTarget.node.id); - if (placement && placement.area == CustomizableUI.AREA_PANEL) { - return true; - } - - let targetElement = aTarget.node; - // Use the widget for filtering if it exists since the target may be the icon inside. - if (aTarget.widgetName) { - targetElement = aTarget.node.ownerDocument.getElementById(aTarget.widgetName); - } - - // Handle the non-customizable buttons at the bottom of the menu which aren't proper widgets. - return targetElement.id.startsWith("PanelUI-") - && targetElement.id != "PanelUI-button"; - }, - - /** - * Called before opening or after closing a highlight or info panel to see if - * we need to open or close the appMenu to see the annotation's anchor. - */ - _setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) { - log.debug("_setAppMenuStateForAnnotation:", aAnnotationType); - log.debug("_setAppMenuStateForAnnotation: Menu is expected to be:", aShouldOpenForHighlight ? "open" : "closed"); - - // If the panel is in the desired state, we're done. - let panelIsOpen = aWindow.PanelUI.panel.state != "closed"; - if (aShouldOpenForHighlight == panelIsOpen) { - log.debug("_setAppMenuStateForAnnotation: Panel already in expected state"); - if (aCallback) - aCallback(); - return; - } - - // Don't close the menu if it wasn't opened by us (e.g. via showmenu instead). - if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) { - log.debug("_setAppMenuStateForAnnotation: Menu not opened by us, not closing"); - if (aCallback) - aCallback(); - return; - } - - if (aShouldOpenForHighlight) { - this.appMenuOpenForAnnotation.add(aAnnotationType); - } else { - this.appMenuOpenForAnnotation.delete(aAnnotationType); - } - - // Actually show or hide the menu - if (this.appMenuOpenForAnnotation.size) { - log.debug("_setAppMenuStateForAnnotation: Opening the menu"); - this.showMenu(aWindow, "appMenu", aCallback); - } else { - log.debug("_setAppMenuStateForAnnotation: Closing the menu"); - this.hideMenu(aWindow, "appMenu"); - if (aCallback) - aCallback(); - } - - }, - - previewTheme: function(aTheme) { - let origin = Services.prefs.getCharPref("browser.uitour.themeOrigin"); - let data = LightweightThemeManager.parseTheme(aTheme, origin); - if (data) - LightweightThemeManager.previewTheme(data); - }, - - resetTheme: function() { - LightweightThemeManager.resetPreview(); - }, - - /** - * Show the Heartbeat UI to request user feedback. This function reports back to the - * caller using |notify|. The notification event name reflects the current status the UI - * is in (either "Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", - * "Heartbeat:LearnMore", "Heartbeat:Engaged", "Heartbeat:Voted", - * "Heartbeat:SurveyExpired" or "Heartbeat:WindowClosed"). - * When a "Heartbeat:Voted" event is notified - * the data payload contains a |score| field which holds the rating picked by the user. - * Please note that input parameters are already validated by the caller. - * - * @param aChromeWindow - * The chrome window that the heartbeat notification is displayed in. - * @param {Object} aOptions Options object. - * @param {String} aOptions.message - * The message, or question, to display on the notification. - * @param {String} aOptions.thankyouMessage - * The thank you message to display after user votes. - * @param {String} aOptions.flowId - * An identifier for this rating flow. Please note that this is only used to - * identify the notification box. - * @param {String} [aOptions.engagementButtonLabel=null] - * The text of the engagement button to use instad of stars. If this is null - * or invalid, rating stars are used. - * @param {String} [aOptions.engagementURL=null] - * The engagement URL to open in a new tab once user has engaged. If this is null - * or invalid, no new tab is opened. - * @param {String} [aOptions.learnMoreLabel=null] - * The label of the learn more link. No link will be shown if this is null. - * @param {String} [aOptions.learnMoreURL=null] - * The learn more URL to open when clicking on the learn more link. No learn more - * will be shown if this is an invalid URL. - * @param {boolean} [aOptions.privateWindowsOnly=false] - * Whether the heartbeat UI should only be targeted at a private window (if one exists). - * No notifications should be fired when this is true. - * @param {String} [aOptions.surveyId] - * An ID for the survey, reflected in the Telemetry ping. - * @param {Number} [aOptions.surveyVersion] - * Survey's version number, reflected in the Telemetry ping. - * @param {boolean} [aOptions.testing] - * Whether this is a test survey, reflected in the Telemetry ping. - */ - showHeartbeat(aChromeWindow, aOptions) { - // Initialize survey state - let pingSent = false; - let surveyResults = {}; - let surveyEndTimer = null; - - /** - * Accumulates survey events and submits to Telemetry after the survey ends. - * - * @param {String} aEventName - * Heartbeat event name - * @param {Object} aParams - * Additional parameters and their values - */ - let maybeNotifyHeartbeat = (aEventName, aParams = {}) => { - // Return if event occurred after the ping was sent - if (pingSent) { - log.warn("maybeNotifyHeartbeat: event occurred after ping sent:", aEventName, aParams); - return; - } - - // No Telemetry from private-window-only Heartbeats - if (aOptions.privateWindowsOnly) { - return; - } - - let ts = Date.now(); - let sendPing = false; - switch (aEventName) { - case "Heartbeat:NotificationOffered": - surveyResults.flowId = aOptions.flowId; - surveyResults.offeredTS = ts; - break; - case "Heartbeat:LearnMore": - // record only the first click - if (!surveyResults.learnMoreTS) { - surveyResults.learnMoreTS = ts; - } - break; - case "Heartbeat:Engaged": - surveyResults.engagedTS = ts; - break; - case "Heartbeat:Voted": - surveyResults.votedTS = ts; - surveyResults.score = aParams.score; - break; - case "Heartbeat:SurveyExpired": - surveyResults.expiredTS = ts; - break; - case "Heartbeat:NotificationClosed": - // this is the final event in most surveys - surveyResults.closedTS = ts; - sendPing = true; - break; - case "Heartbeat:WindowClosed": - surveyResults.windowClosedTS = ts; - sendPing = true; - break; - default: - log.error("maybeNotifyHeartbeat: unrecognized event:", aEventName); - break; - } - - aParams.timestamp = ts; - aParams.flowId = aOptions.flowId; - this.notify(aEventName, aParams); - - if (!sendPing) { - return; - } - - // Send the ping to Telemetry - let payload = Object.assign({}, surveyResults); - payload.version = 1; - for (let meta of ["surveyId", "surveyVersion", "testing"]) { - if (aOptions.hasOwnProperty(meta)) { - payload[meta] = aOptions[meta]; - } - } - - log.debug("Sending payload to Telemetry: aEventName:", aEventName, - "payload:", payload); - - TelemetryController.submitExternalPing("heartbeat", payload, { - addClientId: true, - addEnvironment: true, - }); - - // only for testing - this.notify("Heartbeat:TelemetrySent", payload); - - // Survey is complete, clear out the expiry timer & survey configuration - if (surveyEndTimer) { - clearTimeout(surveyEndTimer); - surveyEndTimer = null; - } - - pingSent = true; - surveyResults = {}; - }; - - let nb = aChromeWindow.document.getElementById("high-priority-global-notificationbox"); - let buttons = null; - - if (aOptions.engagementButtonLabel) { - buttons = [{ - label: aOptions.engagementButtonLabel, - callback: () => { - // Let the consumer know user engaged. - maybeNotifyHeartbeat("Heartbeat:Engaged"); - - userEngaged(new Map([ - ["type", "button"], - ["flowid", aOptions.flowId] - ])); - - // Return true so that the notification bar doesn't close itself since - // we have a thank you message to show. - return true; - }, - }]; - } - - let defaultIcon = "chrome://browser/skin/heartbeat-icon.svg"; - let iconURL = defaultIcon; - try { - // Take the optional icon URL if specified - if (aOptions.iconURL) { - iconURL = new URL(aOptions.iconURL); - // For now, only allow chrome URIs. - if (iconURL.protocol != "chrome:") { - iconURL = defaultIcon; - throw new Error("Invalid protocol"); - } - } - } catch (error) { - log.error("showHeartbeat: Invalid icon URL specified."); - } - - // Create the notification. Prefix its ID to decrease the chances of collisions. - let notice = nb.appendNotification(aOptions.message, "heartbeat-" + aOptions.flowId, - iconURL, - nb.PRIORITY_INFO_HIGH, buttons, - (aEventType) => { - if (aEventType != "removed") { - return; - } - // Let the consumer know the notification bar was closed. - // This also happens after voting. - maybeNotifyHeartbeat("Heartbeat:NotificationClosed"); - }); - - // Get the elements we need to style. - let messageImage = - aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageImage"); - let messageText = - aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageText"); - - function userEngaged(aEngagementParams) { - // Make the heartbeat icon pulse twice. - notice.label = aOptions.thankyouMessage; - messageImage.classList.remove("pulse-onshow"); - messageImage.classList.add("pulse-twice"); - - // Remove all the children of the notice (rating container - // and the flex). - while (notice.firstChild) { - notice.removeChild(notice.firstChild); - } - - // Make sure that we have a valid URL. If we haven't, do not open the engagement page. - let engagementURL = null; - try { - engagementURL = new URL(aOptions.engagementURL); - } catch (error) { - log.error("showHeartbeat: Invalid URL specified."); - } - - // Just open the engagement tab if we have a valid engagement URL. - if (engagementURL) { - for (let [param, value] of aEngagementParams) { - engagementURL.searchParams.append(param, value); - } - - // Open the engagement URL in a new tab. - aChromeWindow.gBrowser.selectedTab = - aChromeWindow.gBrowser.addTab(engagementURL.toString(), { - owner: aChromeWindow.gBrowser.selectedTab, - relatedToCurrent: true - }); - } - - // Remove the notification bar after 3 seconds. - aChromeWindow.setTimeout(() => { - nb.removeNotification(notice); - }, 3000); - } - - // Create the fragment holding the rating UI. - let frag = aChromeWindow.document.createDocumentFragment(); - - // Build the Heartbeat star rating. - const numStars = aOptions.engagementButtonLabel ? 0 : 5; - let ratingContainer = aChromeWindow.document.createElement("hbox"); - ratingContainer.id = "star-rating-container"; - - for (let i = 0; i < numStars; i++) { - // Create a star rating element. - let ratingElement = aChromeWindow.document.createElement("toolbarbutton"); - - // Style it. - let starIndex = numStars - i; - ratingElement.className = "plain star-x"; - ratingElement.id = "star" + starIndex; - ratingElement.setAttribute("data-score", starIndex); - - // Add the click handler. - ratingElement.addEventListener("click", function (evt) { - let rating = Number(evt.target.getAttribute("data-score"), 10); - - // Let the consumer know user voted. - maybeNotifyHeartbeat("Heartbeat:Voted", { score: rating }); - - // Append the score data to the engagement URL. - userEngaged(new Map([ - ["type", "stars"], - ["score", rating], - ["flowid", aOptions.flowId] - ])); - }.bind(this)); - - // Add it to the container. - ratingContainer.appendChild(ratingElement); - } - - frag.appendChild(ratingContainer); - - // Make sure the stars are not pushed to the right by the spacer. - let rightSpacer = aChromeWindow.document.createElement("spacer"); - rightSpacer.flex = 20; - frag.appendChild(rightSpacer); - - messageText.flex = 0; // Collapse the space before the stars. - let leftSpacer = messageText.nextSibling; - leftSpacer.flex = 0; - - // Make sure that we have a valid learn more URL. - let learnMoreURL = null; - try { - learnMoreURL = new URL(aOptions.learnMoreURL); - } catch (error) { - log.error("showHeartbeat: Invalid learnMore URL specified."); - } - - // Add the learn more link. - if (aOptions.learnMoreLabel && learnMoreURL) { - let learnMore = aChromeWindow.document.createElement("label"); - learnMore.className = "text-link"; - learnMore.href = learnMoreURL.toString(); - learnMore.setAttribute("value", aOptions.learnMoreLabel); - learnMore.addEventListener("click", () => maybeNotifyHeartbeat("Heartbeat:LearnMore")); - frag.appendChild(learnMore); - } - - // Append the fragment and apply the styling. - notice.appendChild(frag); - notice.classList.add("heartbeat"); - messageImage.classList.add("heartbeat", "pulse-onshow"); - messageText.classList.add("heartbeat"); - - // Let the consumer know the notification was shown. - maybeNotifyHeartbeat("Heartbeat:NotificationOffered"); - - // End the survey if the user quits, closes the window, or - // hasn't responded before expiration. - if (!aOptions.privateWindowsOnly) { - function handleWindowClosed(aTopic) { - maybeNotifyHeartbeat("Heartbeat:WindowClosed"); - aChromeWindow.removeEventListener("SSWindowClosing", handleWindowClosed); - } - aChromeWindow.addEventListener("SSWindowClosing", handleWindowClosed); - - let surveyDuration = Services.prefs.getIntPref(PREF_SURVEY_DURATION) * 1000; - surveyEndTimer = setTimeout(() => { - maybeNotifyHeartbeat("Heartbeat:SurveyExpired"); - nb.removeNotification(notice); - }, surveyDuration); - } - }, - - /** - * The node to which a highlight or notification(-popup) is anchored is sometimes - * obscured because it may be inside an overflow menu. This function should figure - * that out and offer the overflow chevron as an alternative. - * - * @param {Node} aAnchor The element that's supposed to be the anchor - * @type {Node} - */ - _correctAnchor: function(aAnchor) { - // If the target is in the overflow panel, just return the overflow button. - if (aAnchor.getAttribute("overflowedItem")) { - let doc = aAnchor.ownerDocument; - let placement = CustomizableUI.getPlacementOfWidget(aAnchor.id); - let areaNode = doc.getElementById(placement.area); - return areaNode.overflowable._chevron; - } - - return aAnchor; - }, - - /** - * @param aChromeWindow The chrome window that the highlight is in. Necessary since some targets - * are in a sub-frame so the defaultView is not the same as the chrome - * window. - * @param aTarget The element to highlight. - * @param aEffect (optional) The effect to use from UITour.highlightEffects or "none". - * @see UITour.highlightEffects - */ - showHighlight: function(aChromeWindow, aTarget, aEffect = "none") { - function showHighlightPanel() { - let highlighter = aChromeWindow.document.getElementById("UITourHighlight"); - - let effect = aEffect; - if (effect == "random") { - // Exclude "random" from the randomly selected effects. - let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1)); - if (randomEffect == this.highlightEffects.length) - randomEffect--; // On the order of 1 in 2^62 chance of this happening. - effect = this.highlightEffects[randomEffect]; - } - // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays. - highlighter.setAttribute("active", "none"); - aChromeWindow.getComputedStyle(highlighter).animationName; - highlighter.setAttribute("active", effect); - highlighter.parentElement.setAttribute("targetName", aTarget.targetName); - highlighter.parentElement.hidden = false; - - let highlightAnchor = this._correctAnchor(aTarget.node); - let targetRect = highlightAnchor.getBoundingClientRect(); - let highlightHeight = targetRect.height; - let highlightWidth = targetRect.width; - let minDimension = Math.min(highlightHeight, highlightWidth); - let maxDimension = Math.max(highlightHeight, highlightWidth); - - // If the dimensions are within 200% of each other (to include the bookmarks button), - // make the highlight a circle with the largest dimension as the diameter. - if (maxDimension / minDimension <= 3.0) { - highlightHeight = highlightWidth = maxDimension; - highlighter.style.borderRadius = "100%"; - } else { - highlighter.style.borderRadius = ""; - } - - highlighter.style.height = highlightHeight + "px"; - highlighter.style.width = highlightWidth + "px"; - - // Close a previous highlight so we can relocate the panel. - if (highlighter.parentElement.state == "showing" || highlighter.parentElement.state == "open") { - log.debug("showHighlight: Closing previous highlight first"); - highlighter.parentElement.hidePopup(); - } - /* The "overlap" position anchors from the top-left but we want to centre highlights at their - minimum size. */ - let highlightWindow = aChromeWindow; - let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement); - let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop); - let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft); - let highlightStyle = highlightWindow.getComputedStyle(highlighter); - let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight)); - let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth)); - let offsetX = paddingTopPx - - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2); - let offsetY = paddingLeftPx - - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2); - this._addAnnotationPanelMutationObserver(highlighter.parentElement); - highlighter.parentElement.openPopup(highlightAnchor, "overlap", offsetX, offsetY); - } - - // Prevent showing a panel at an undefined position. - if (!this.isElementVisible(aTarget.node)) { - log.warn("showHighlight: Not showing a highlight since the target isn't visible", aTarget); - return; - } - - this._setAppMenuStateForAnnotation(aChromeWindow, "highlight", - this.targetIsInAppMenu(aTarget), - showHighlightPanel.bind(this)); - }, - - hideHighlight: function(aWindow) { - let highlighter = aWindow.document.getElementById("UITourHighlight"); - this._removeAnnotationPanelMutationObserver(highlighter.parentElement); - highlighter.parentElement.hidePopup(); - highlighter.removeAttribute("active"); - - this._setAppMenuStateForAnnotation(aWindow, "highlight", false); - }, - - /** - * Show an info panel. - * - * @param {ChromeWindow} aChromeWindow - * @param {Node} aAnchor - * @param {String} [aTitle=""] - * @param {String} [aDescription=""] - * @param {String} [aIconURL=""] - * @param {Object[]} [aButtons=[]] - * @param {Object} [aOptions={}] - * @param {String} [aOptions.closeButtonCallback] - * @param {String} [aOptions.targetCallback] - */ - showInfo(aChromeWindow, aAnchor, aTitle = "", aDescription = "", - aIconURL = "", aButtons = [], aOptions = {}) { - function showInfoPanel(aAnchorEl) { - aAnchorEl.focus(); - - let document = aChromeWindow.document; - let tooltip = document.getElementById("UITourTooltip"); - let tooltipTitle = document.getElementById("UITourTooltipTitle"); - let tooltipDesc = document.getElementById("UITourTooltipDescription"); - let tooltipIcon = document.getElementById("UITourTooltipIcon"); - let tooltipButtons = document.getElementById("UITourTooltipButtons"); - - if (tooltip.state == "showing" || tooltip.state == "open") { - tooltip.hidePopup(); - } - - tooltipTitle.textContent = aTitle || ""; - tooltipDesc.textContent = aDescription || ""; - tooltipIcon.src = aIconURL || ""; - tooltipIcon.hidden = !aIconURL; - - while (tooltipButtons.firstChild) - tooltipButtons.firstChild.remove(); - - for (let button of aButtons) { - let isButton = button.style != "text"; - let el = document.createElement(isButton ? "button" : "label"); - el.setAttribute(isButton ? "label" : "value", button.label); - - if (isButton) { - if (button.iconURL) - el.setAttribute("image", button.iconURL); - - if (button.style == "link") - el.setAttribute("class", "button-link"); - - if (button.style == "primary") - el.setAttribute("class", "button-primary"); - - // Don't close the popup or call the callback for style=text as they - // aren't links/buttons. - let callback = button.callback; - el.addEventListener("command", event => { - tooltip.hidePopup(); - callback(event); - }); - } - - tooltipButtons.appendChild(el); - } - - tooltipButtons.hidden = !aButtons.length; - - let tooltipClose = document.getElementById("UITourTooltipClose"); - let closeButtonCallback = (event) => { - this.hideInfo(document.defaultView); - if (aOptions && aOptions.closeButtonCallback) { - aOptions.closeButtonCallback(); - } - }; - tooltipClose.addEventListener("command", closeButtonCallback); - - let targetCallback = (event) => { - let details = { - target: aAnchor.targetName, - type: event.type, - }; - aOptions.targetCallback(details); - }; - if (aOptions.targetCallback && aAnchor.addTargetListener) { - aAnchor.addTargetListener(document, targetCallback); - } - - tooltip.addEventListener("popuphiding", function tooltipHiding(event) { - tooltip.removeEventListener("popuphiding", tooltipHiding); - tooltipClose.removeEventListener("command", closeButtonCallback); - if (aOptions.targetCallback && aAnchor.removeTargetListener) { - aAnchor.removeTargetListener(document, targetCallback); - } - }); - - tooltip.setAttribute("targetName", aAnchor.targetName); - tooltip.hidden = false; - let alignment = "bottomcenter topright"; - if (aAnchor.infoPanelPosition) { - alignment = aAnchor.infoPanelPosition; - } - - let { infoPanelOffsetX: xOffset, infoPanelOffsetY: yOffset } = aAnchor; - - this._addAnnotationPanelMutationObserver(tooltip); - tooltip.openPopup(aAnchorEl, alignment, xOffset || 0, yOffset || 0); - if (tooltip.state == "closed") { - document.defaultView.addEventListener("endmodalstate", function endModalStateHandler() { - document.defaultView.removeEventListener("endmodalstate", endModalStateHandler); - tooltip.openPopup(aAnchorEl, alignment); - }, false); - } - } - - // Prevent showing a panel at an undefined position. - if (!this.isElementVisible(aAnchor.node)) { - log.warn("showInfo: Not showing since the target isn't visible", aAnchor); - return; - } - - this._setAppMenuStateForAnnotation(aChromeWindow, "info", - this.targetIsInAppMenu(aAnchor), - showInfoPanel.bind(this, this._correctAnchor(aAnchor.node))); - }, - - isInfoOnTarget(aChromeWindow, aTargetName) { - let document = aChromeWindow.document; - let tooltip = document.getElementById("UITourTooltip"); - return tooltip.getAttribute("targetName") == aTargetName && tooltip.state != "closed"; - }, - - hideInfo: function(aWindow) { - let document = aWindow.document; - - let tooltip = document.getElementById("UITourTooltip"); - this._removeAnnotationPanelMutationObserver(tooltip); - tooltip.hidePopup(); - this._setAppMenuStateForAnnotation(aWindow, "info", false); - - let tooltipButtons = document.getElementById("UITourTooltipButtons"); - while (tooltipButtons.firstChild) - tooltipButtons.firstChild.remove(); - }, - - showMenu: function(aWindow, aMenuName, aOpenCallback = null) { - log.debug("showMenu:", aMenuName); - function openMenuButton(aMenuBtn) { - if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) { - if (aOpenCallback) - aOpenCallback(); - return; - } - if (aOpenCallback) - aMenuBtn.addEventListener("popupshown", onPopupShown); - aMenuBtn.boxObject.openMenu(true); - } - function onPopupShown(event) { - this.removeEventListener("popupshown", onPopupShown); - aOpenCallback(event); - } - - if (aMenuName == "appMenu") { - aWindow.PanelUI.panel.setAttribute("noautohide", "true"); - // If the popup is already opened, don't recreate the widget as it may cause a flicker. - if (aWindow.PanelUI.panel.state != "open") { - this.recreatePopup(aWindow.PanelUI.panel); - } - aWindow.PanelUI.panel.addEventListener("popuphiding", this.hideAppMenuAnnotations); - aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hideAppMenuAnnotations); - aWindow.PanelUI.panel.addEventListener("popuphidden", this.onPanelHidden); - if (aOpenCallback) { - aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown); - } - aWindow.PanelUI.show(); - } else if (aMenuName == "bookmarks") { - let menuBtn = aWindow.document.getElementById("bookmarks-menu-button"); - openMenuButton(menuBtn); - } else if (aMenuName == "controlCenter") { - let popup = aWindow.gIdentityHandler._identityPopup; - - // Add the listener even if the panel is already open since it will still - // only get registered once even if it was UITour that opened it. - popup.addEventListener("popuphiding", this.hideControlCenterAnnotations); - popup.addEventListener("popuphidden", this.onPanelHidden); - - popup.setAttribute("noautohide", true); - this.clearAvailableTargetsCache(); - - if (popup.state == "open") { - if (aOpenCallback) { - aOpenCallback(); - } - return; - } - - this.recreatePopup(popup); - - // Open the control center - if (aOpenCallback) { - popup.addEventListener("popupshown", onPopupShown); - } - aWindow.document.getElementById("identity-box").click(); - } else if (aMenuName == "pocket") { - this.getTarget(aWindow, "pocket").then(Task.async(function* onPocketTarget(target) { - let widgetGroupWrapper = CustomizableUI.getWidget(target.widgetName); - if (widgetGroupWrapper.type != "view" || !widgetGroupWrapper.viewId) { - log.error("Can't open the pocket menu without a view"); - return; - } - let placement = CustomizableUI.getPlacementOfWidget(target.widgetName); - if (!placement || !placement.area) { - log.error("Can't open the pocket menu without a placement"); - return; - } - - if (placement.area == CustomizableUI.AREA_PANEL) { - // Open the appMenu and wait for it if it's not already opened or showing a subview. - yield new Promise((resolve, reject) => { - if (aWindow.PanelUI.panel.state != "closed") { - if (aWindow.PanelUI.multiView.showingSubView) { - reject("A subview is already showing"); - return; - } - - resolve(); - return; - } - - aWindow.PanelUI.panel.addEventListener("popupshown", function onShown() { - aWindow.PanelUI.panel.removeEventListener("popupshown", onShown); - resolve(); - }); - - aWindow.PanelUI.show(); - }); - } - - let widgetWrapper = widgetGroupWrapper.forWindow(aWindow); - aWindow.PanelUI.showSubView(widgetGroupWrapper.viewId, - widgetWrapper.anchor, - placement.area); - })).catch(log.error); - } - }, - - hideMenu: function(aWindow, aMenuName) { - log.debug("hideMenu:", aMenuName); - function closeMenuButton(aMenuBtn) { - if (aMenuBtn && aMenuBtn.boxObject) - aMenuBtn.boxObject.openMenu(false); - } - - if (aMenuName == "appMenu") { - aWindow.PanelUI.hide(); - } else if (aMenuName == "bookmarks") { - let menuBtn = aWindow.document.getElementById("bookmarks-menu-button"); - closeMenuButton(menuBtn); - } else if (aMenuName == "controlCenter") { - let panel = aWindow.gIdentityHandler._identityPopup; - panel.hidePopup(); - } - }, - - showNewTab: function(aWindow, aBrowser) { - aWindow.openLinkIn("about:newtab", "current", {targetBrowser: aBrowser}); - }, - - hideAnnotationsForPanel: function(aEvent, aTargetPositionCallback) { - let win = aEvent.target.ownerGlobal; - let annotationElements = new Map([ - // [annotationElement (panel), method to hide the annotation] - [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)], - [win.document.getElementById("UITourTooltip"), UITour.hideInfo.bind(UITour)], - ]); - annotationElements.forEach((hideMethod, annotationElement) => { - if (annotationElement.state != "closed") { - let targetName = annotationElement.getAttribute("targetName"); - UITour.getTarget(win, targetName).then((aTarget) => { - // Since getTarget is async, we need to make sure that the target hasn't - // changed since it may have just moved to somewhere outside of the app menu. - if (annotationElement.getAttribute("targetName") != aTarget.targetName || - annotationElement.state == "closed" || - !aTargetPositionCallback(aTarget)) { - return; - } - hideMethod(win); - }).catch(log.error); - } - }); - UITour.appMenuOpenForAnnotation.clear(); - }, - - hideAppMenuAnnotations: function(aEvent) { - UITour.hideAnnotationsForPanel(aEvent, UITour.targetIsInAppMenu); - }, - - hideControlCenterAnnotations(aEvent) { - UITour.hideAnnotationsForPanel(aEvent, (aTarget) => { - return aTarget.targetName.startsWith("controlCenter-"); - }); - }, - - onPanelHidden: function(aEvent) { - aEvent.target.removeAttribute("noautohide"); - UITour.recreatePopup(aEvent.target); - UITour.clearAvailableTargetsCache(); - }, - - recreatePopup: function(aPanel) { - // After changing popup attributes that relate to how the native widget is created - // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect. - if (aPanel.hidden) { - // If the panel is already hidden, we don't need to recreate it but flush - // in case someone just hid it. - aPanel.clientWidth; // flush - return; - } - aPanel.hidden = true; - aPanel.clientWidth; // flush - aPanel.hidden = false; - }, - - getConfiguration: function(aMessageManager, aWindow, aConfiguration, aCallbackID) { - switch (aConfiguration) { - case "appinfo": - let props = ["defaultUpdateChannel", "version"]; - let appinfo = {}; - props.forEach(property => appinfo[property] = Services.appinfo[property]); - - // Identifier of the partner repack, as stored in preference "distribution.id" - // and included in Firefox and other update pings. Note this is not the same as - // Services.appinfo.distributionID (value of MOZ_DISTRIBUTION_ID is set at build time). - let distribution = "default"; - try { - distribution = Services.prefs.getDefaultBranch("distribution.").getCharPref("id"); - } catch (e) {} - appinfo["distribution"] = distribution; - - let isDefaultBrowser = null; - try { - let shell = aWindow.getShellService(); - if (shell) { - isDefaultBrowser = shell.isDefaultBrowser(false); - } - } catch (e) {} - appinfo["defaultBrowser"] = isDefaultBrowser; - - let canSetDefaultBrowserInBackground = true; - if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2") || - AppConstants.isPlatformAndVersionAtLeast("macosx", "10.10")) { - canSetDefaultBrowserInBackground = false; - } else if (AppConstants.platform == "linux") { - // The ShellService may not exist on some versions of Linux. - try { - aWindow.getShellService(); - } catch (e) { - canSetDefaultBrowserInBackground = null; - } - } - - appinfo["canSetDefaultBrowserInBackground"] = - canSetDefaultBrowserInBackground; - - this.sendPageCallback(aMessageManager, aCallbackID, appinfo); - break; - case "availableTargets": - this.getAvailableTargets(aMessageManager, aWindow, aCallbackID); - break; - case "search": - case "selectedSearchEngine": - Services.search.init(rv => { - let data; - if (Components.isSuccessCode(rv)) { - let engines = Services.search.getVisibleEngines(); - data = { - searchEngineIdentifier: Services.search.defaultEngine.identifier, - engines: engines.filter((engine) => engine.identifier) - .map((engine) => TARGET_SEARCHENGINE_PREFIX + engine.identifier) - }; - } else { - data = {engines: [], searchEngineIdentifier: ""}; - } - this.sendPageCallback(aMessageManager, aCallbackID, data); - }); - break; - case "sync": - this.sendPageCallback(aMessageManager, aCallbackID, { - setup: Services.prefs.prefHasUserValue("services.sync.username"), - desktopDevices: Preferences.get("services.sync.clients.devices.desktop", 0), - mobileDevices: Preferences.get("services.sync.clients.devices.mobile", 0), - totalDevices: Preferences.get("services.sync.numClients", 0), - }); - break; - case "canReset": - this.sendPageCallback(aMessageManager, aCallbackID, ResetProfile.resetSupported()); - break; - default: - log.error("getConfiguration: Unknown configuration requested: " + aConfiguration); - break; - } - }, - - setConfiguration: function(aWindow, aConfiguration, aValue) { - switch (aConfiguration) { - case "defaultBrowser": - // Ignore aValue in this case because the default browser can only - // be set, not unset. - try { - let shell = aWindow.getShellService(); - if (shell) { - shell.setDefaultBrowser(true, false); - } - } catch (e) {} - break; - default: - log.error("setConfiguration: Unknown configuration requested: " + aConfiguration); - break; - } - }, - - getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) { - Task.spawn(function*() { - let window = aChromeWindow; - let data = this.availableTargetsCache.get(window); - if (data) { - log.debug("getAvailableTargets: Using cached targets list", data.targets.join(",")); - this.sendPageCallback(aMessageManager, aCallbackID, data); - return; - } - - let promises = []; - for (let targetName of this.targets.keys()) { - promises.push(this.getTarget(window, targetName)); - } - let targetObjects = yield Promise.all(promises); - - let targetNames = []; - for (let targetObject of targetObjects) { - if (targetObject.node) - targetNames.push(targetObject.targetName); - } - - data = { - targets: targetNames, - }; - this.availableTargetsCache.set(window, data); - this.sendPageCallback(aMessageManager, aCallbackID, data); - }.bind(this)).catch(err => { - log.error(err); - this.sendPageCallback(aMessageManager, aCallbackID, { - targets: [], - }); - }); - }, - - startSubTour: function (aFeature) { - if (aFeature != "string") { - log.error("startSubTour: No feature option specified"); - return; - } - - if (aFeature == "readinglist") { - ReaderParent.showReaderModeInfoPanel(browser); - } else { - log.error("startSubTour: Unknown feature option specified"); - return; - } - }, - - addNavBarWidget: function (aTarget, aMessageManager, aCallbackID) { - if (aTarget.node) { - log.error("addNavBarWidget: can't add a widget already present:", aTarget); - return; - } - if (!aTarget.allowAdd) { - log.error("addNavBarWidget: not allowed to add this widget:", aTarget); - return; - } - if (!aTarget.widgetName) { - log.error("addNavBarWidget: can't add a widget without a widgetName property:", aTarget); - return; - } - - CustomizableUI.addWidgetToArea(aTarget.widgetName, CustomizableUI.AREA_NAVBAR); - this.sendPageCallback(aMessageManager, aCallbackID); - }, - - _addAnnotationPanelMutationObserver: function(aPanelEl) { - if (AppConstants.platform == "linux") { - let observer = this._annotationPanelMutationObservers.get(aPanelEl); - if (observer) { - return; - } - let win = aPanelEl.ownerGlobal; - observer = new win.MutationObserver(this._annotationMutationCallback); - this._annotationPanelMutationObservers.set(aPanelEl, observer); - let observerOptions = { - attributeFilter: ["height", "width"], - attributes: true, - }; - observer.observe(aPanelEl, observerOptions); - } - }, - - _removeAnnotationPanelMutationObserver: function(aPanelEl) { - if (AppConstants.platform == "linux") { - let observer = this._annotationPanelMutationObservers.get(aPanelEl); - if (observer) { - observer.disconnect(); - this._annotationPanelMutationObservers.delete(aPanelEl); - } - } - }, - -/** - * Workaround for Ubuntu panel craziness in bug 970788 where incorrect sizes get passed to - * nsXULPopupManager::PopupResized and lead to incorrect width and height attributes getting - * set on the panel. - */ - _annotationMutationCallback: function(aMutations) { - for (let mutation of aMutations) { - // Remove both attributes at once and ignore remaining mutations to be proccessed. - mutation.target.removeAttribute("width"); - mutation.target.removeAttribute("height"); - return; - } - }, - - selectSearchEngine(aID) { - return new Promise((resolve, reject) => { - Services.search.init((rv) => { - if (!Components.isSuccessCode(rv)) { - reject("selectSearchEngine: search service init failed: " + rv); - return; - } - - let engines = Services.search.getVisibleEngines(); - for (let engine of engines) { - if (engine.identifier == aID) { - Services.search.defaultEngine = engine; - resolve(); - return; - } - } - reject("selectSearchEngine could not find engine with given ID"); - return; - }); - }); - }, - - notify(eventName, params) { - let winEnum = Services.wm.getEnumerator("navigator:browser"); - while (winEnum.hasMoreElements()) { - let window = winEnum.getNext(); - if (window.closed) - continue; - - let openTourBrowsers = this.tourBrowsersByWindow.get(window); - if (!openTourBrowsers) - continue; - - for (let browser of openTourBrowsers) { - let messageManager = browser.messageManager; - if (!messageManager) { - log.error("notify: Trying to notify a browser without a messageManager", browser); - continue; - } - let detail = { - event: eventName, - params: params, - }; - messageManager.sendAsyncMessage("UITour:SendPageNotification", detail); - } - } - }, -}; - -function controlCenterTrackingToggleTarget(aUnblock) { - return { - infoPanelPosition: "rightcenter topleft", - query(aDocument) { - let popup = aDocument.defaultView.gIdentityHandler._identityPopup; - if (popup.state != "open") { - return null; - } - let buttonId = null; - if (aUnblock) { - if (PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)) { - buttonId = "tracking-action-unblock-private"; - } else { - buttonId = "tracking-action-unblock"; - } - } else { - buttonId = "tracking-action-block"; - } - let element = aDocument.getElementById(buttonId); - return UITour.isElementVisible(element) ? element : null; - }, - }; -} - -this.UITour.init(); - -/** - * UITour Health Report - */ -/** - * Public API to be called by the UITour code - */ -const UITourHealthReport = { - recordTreatmentTag: function(tag, value) { - return TelemetryController.submitExternalPing("uitour-tag", - { - version: 1, - tagName: tag, - tagValue: value, - }, - { - addClientId: true, - addEnvironment: true, - }); - } -}; |