/* This Source Code Form is subject to the terms of the Mozilla PublicddonMa * 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/. */ const FXA_ENABLED_PREF = "identity.fxaccounts.enabled"; const DISTRIBUTION_ID_PREF = "distribution.id"; const DISTRIBUTION_ID_CHINA_REPACK = "MozillaOnline"; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { NewTabUtils } = ChromeUtils.importESModule( "resource://gre/modules/NewTabUtils.sys.mjs" ); const { ShellService } = ChromeUtils.importESModule( "resource:///modules/ShellService.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AddonManager: "resource://gre/modules/AddonManager.sys.mjs", AttributionCode: "resource:///modules/AttributionCode.sys.mjs", ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs", CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", }); XPCOMUtils.defineLazyModuleGetters(lazy, { ASRouterPreferences: "resource://activity-stream/lib/ASRouterPreferences.jsm", HomePage: "resource:///modules/HomePage.jsm", AboutNewTab: "resource:///modules/AboutNewTab.jsm", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", }); XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => { return ChromeUtils.importESModule( "resource://gre/modules/FxAccounts.sys.mjs" ).getFxAccountsSingleton(); }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "cfrFeaturesUserPref", "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", true ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "cfrAddonsUserPref", "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", true ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "isWhatsNewPanelEnabled", "browser.messaging-system.whatsNewPanel.enabled", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "hasAccessedFxAPanel", "identity.fxaccounts.toolbar.accessed", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "clientsDevicesDesktop", "services.sync.clients.devices.desktop", 0 ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "clientsDevicesMobile", "services.sync.clients.devices.mobile", 0 ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "syncNumClients", "services.sync.numClients", 0 ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "devtoolsSelfXSSCount", "devtools.selfxss.count", 0 ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "isFxAEnabled", FXA_ENABLED_PREF, true ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "isXPIInstallEnabled", "xpinstall.enabled", true ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "snippetsUserPref", "browser.newtabpage.activity-stream.feeds.snippets", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "hasMigratedBookmarks", "browser.migrate.interactions.bookmarks", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "hasMigratedHistory", "browser.migrate.interactions.history", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "hasMigratedPasswords", "browser.migrate.interactions.passwords", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "useEmbeddedMigrationWizard", "browser.migrate.content-modal.about-welcome-behavior", "default", null, behaviorString => { return behaviorString === "embedded"; } ); XPCOMUtils.defineLazyServiceGetters(lazy, { AUS: ["@mozilla.org/updates/update-service;1", "nsIApplicationUpdateService"], BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"], TrackingDBService: [ "@mozilla.org/tracking-db-service;1", "nsITrackingDBService", ], UpdateCheckSvc: ["@mozilla.org/updates/update-checker;1", "nsIUpdateChecker"], }); const FXA_USERNAME_PREF = "services.sync.username"; const { activityStreamProvider: asProvider } = NewTabUtils; const FXA_ATTACHED_CLIENTS_UPDATE_INTERVAL = 4 * 60 * 60 * 1000; // Four hours const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours const FRECENT_SITES_IGNORE_BLOCKED = false; const FRECENT_SITES_NUM_ITEMS = 25; const FRECENT_SITES_MIN_FRECENCY = 100; const CACHE_EXPIRATION = 5 * 60 * 1000; const jexlEvaluationCache = new Map(); /** * CachedTargetingGetter * @param property {string} Name of the method * @param options {any=} Options passed to the method * @param updateInterval {number?} Update interval for query. Defaults to FRECENT_SITES_UPDATE_INTERVAL */ function CachedTargetingGetter( property, options = null, updateInterval = FRECENT_SITES_UPDATE_INTERVAL, getter = asProvider ) { return { _lastUpdated: 0, _value: null, // For testing expire() { this._lastUpdated = 0; this._value = null; }, async get() { const now = Date.now(); if (now - this._lastUpdated >= updateInterval) { this._value = await getter[property](options); this._lastUpdated = now; } return this._value; }, }; } function CacheListAttachedOAuthClients() { return { _lastUpdated: 0, _value: null, expire() { this._lastUpdated = 0; this._value = null; }, get() { const now = Date.now(); if (now - this._lastUpdated >= FXA_ATTACHED_CLIENTS_UPDATE_INTERVAL) { this._value = new Promise(resolve => { lazy.fxAccounts .listAttachedOAuthClients() .then(clients => { resolve(clients); }) .catch(() => resolve([])); }); this._lastUpdated = now; } return this._value; }, }; } function CheckBrowserNeedsUpdate( updateInterval = FRECENT_SITES_UPDATE_INTERVAL ) { const checker = { _lastUpdated: 0, _value: null, // For testing. Avoid update check network call. setUp(value) { this._lastUpdated = Date.now(); this._value = value; }, expire() { this._lastUpdated = 0; this._value = null; }, async get() { const now = Date.now(); if ( !AppConstants.MOZ_UPDATER || now - this._lastUpdated < updateInterval ) { return this._value; } if (!lazy.AUS.canCheckForUpdates) { return false; } this._lastUpdated = now; let check = lazy.UpdateCheckSvc.checkForUpdates( lazy.UpdateCheckSvc.FOREGROUND_CHECK ); let result = await check.result; if (!result.succeeded) { throw result.request; } checker._value = !!result.updates.length; return checker._value; }, }; return checker; } const QueryCache = { expireAll() { Object.keys(this.queries).forEach(query => { this.queries[query].expire(); }); Object.keys(this.getters).forEach(key => { this.getters[key].expire(); }); }, queries: { TopFrecentSites: new CachedTargetingGetter("getTopFrecentSites", { ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED, numItems: FRECENT_SITES_NUM_ITEMS, topsiteFrecency: FRECENT_SITES_MIN_FRECENCY, onePerDomain: true, includeFavicon: false, }), TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"), CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(), RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"), ListAttachedOAuthClients: new CacheListAttachedOAuthClients(), UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"), }, getters: { doesAppNeedPin: new CachedTargetingGetter( "doesAppNeedPin", null, FRECENT_SITES_UPDATE_INTERVAL, ShellService ), doesAppNeedPrivatePin: new CachedTargetingGetter( "doesAppNeedPin", true, FRECENT_SITES_UPDATE_INTERVAL, ShellService ), isDefaultBrowser: new CachedTargetingGetter( "isDefaultBrowser", null, FRECENT_SITES_UPDATE_INTERVAL, ShellService ), currentThemes: new CachedTargetingGetter( "getAddonsByTypes", ["theme"], FRECENT_SITES_UPDATE_INTERVAL, lazy.AddonManager // eslint-disable-line mozilla/valid-lazy ), isDefaultHTMLHandler: new CachedTargetingGetter( "isDefaultHandlerFor", [".html"], FRECENT_SITES_UPDATE_INTERVAL, ShellService ), isDefaultPDFHandler: new CachedTargetingGetter( "isDefaultHandlerFor", [".pdf"], FRECENT_SITES_UPDATE_INTERVAL, ShellService ), defaultPDFHandler: new CachedTargetingGetter( "getDefaultPDFHandler", null, FRECENT_SITES_UPDATE_INTERVAL, ShellService ), }, }; /** * sortMessagesByWeightedRank * * Each message has an associated weight, which is guaranteed to be strictly * positive. Sort the messages so that higher weighted messages are more likely * to come first. * * Specifically, sort them so that the probability of message x_1 with weight * w_1 appearing before message x_2 with weight w_2 is (w_1 / (w_1 + w_2)). * * This is equivalent to requiring that x_1 appearing before x_2 is (w_1 / w_2) * "times" as likely as x_2 appearing before x_1. * * See Bug 1484996, Comment 2 for a justification of the method. * * @param {Array} messages - A non-empty array of messages to sort, all with * strictly positive weights * @returns the sorted array */ function sortMessagesByWeightedRank(messages) { return messages .map(message => ({ message, rank: Math.pow(Math.random(), 1 / message.weight), })) .sort((a, b) => b.rank - a.rank) .map(({ message }) => message); } /** * getSortedMessages - Given an array of Messages, applies sorting and filtering rules * in expected order. * * @param {Array} messages * @param {{}} options * @param {boolean} options.ordered - Should .order be used instead of random weighted sorting? * @returns {Array} */ function getSortedMessages(messages, options = {}) { let { ordered } = { ordered: false, ...options }; let result = messages; if (!ordered) { result = sortMessagesByWeightedRank(result); } result.sort((a, b) => { // Next, sort by priority if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) { return -1; } if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) { return 1; } // Sort messages with targeting expressions higher than those with none if (a.targeting && !b.targeting) { return -1; } if (!a.targeting && b.targeting) { return 1; } // Next, sort by order *ascending* if ordered = true if (ordered) { if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) { return 1; } if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) { return -1; } } return 0; }); return result; } /** * parseAboutPageURL - Parse a URL string retrieved from about:home and about:new, returns * its type (web extenstion or custom url) and the parsed url(s) * * @param {string} url - A URL string for home page or newtab page * @returns {Object} { * isWebExt: boolean, * isCustomUrl: boolean, * urls: Array<{url: string, host: string}> * } */ function parseAboutPageURL(url) { let ret = { isWebExt: false, isCustomUrl: false, urls: [], }; if (url.startsWith("moz-extension://")) { ret.isWebExt = true; ret.urls.push({ url, host: "" }); } else { // The home page URL could be either a single URL or a list of "|" separated URLs. // Note that it should work with "about:home" and "about:blank", in which case the // "host" is set as an empty string. for (const _url of url.split("|")) { if (!["about:home", "about:newtab", "about:blank"].includes(_url)) { ret.isCustomUrl = true; } try { const parsedURL = new URL(_url); const host = parsedURL.hostname.replace(/^www\./i, ""); ret.urls.push({ url: _url, host }); } catch (e) {} } // If URL parsing failed, just return the given url with an empty host if (!ret.urls.length) { ret.urls.push({ url, host: "" }); } } return ret; } /** * Get the number of records in autofill storage, e.g. credit cards/addresses. * * @param {Object} [data] * @param {string} [data.collectionName] * The name used to specify which collection to retrieve records. * @param {string} [data.searchString] * The typed string for filtering out the matched records. * @param {string} [data.info] * The input autocomplete property's information. * @returns {Promise} The number of matched records. * @see FormAutofillParent._getRecords */ async function getAutofillRecords(data) { let actor; try { const win = Services.wm.getMostRecentBrowserWindow(); actor = win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( "FormAutofill" ); } catch (error) { // If the actor is not available, we can't get the records. We could import // the records directly from FormAutofillStorage to avoid the messiness of // JSActors, but that would import a lot of code for a targeting attribute. return 0; } let records = await actor?.receiveMessage({ name: "FormAutofill:GetRecords", data, }); return records?.length ?? 0; } // Attribution data can be encoded multiple times so we need this function to // get a cleartext value. function decodeAttributionValue(value) { if (!value) { return null; } let decodedValue = value; while (decodedValue.includes("%")) { try { const result = decodeURIComponent(decodedValue); if (result === decodedValue) { break; } decodedValue = result; } catch (e) { break; } } return decodedValue; } const TargetingGetters = { get locale() { return Services.locale.appLocaleAsBCP47; }, get localeLanguageCode() { return ( Services.locale.appLocaleAsBCP47 && Services.locale.appLocaleAsBCP47.substr(0, 2) ); }, get browserSettings() { const { settings } = lazy.TelemetryEnvironment.currentEnvironment; return { update: settings.update, }; }, get attributionData() { // Attribution is determined at startup - so we can use the cached attribution at this point return lazy.AttributionCode.getCachedAttributionData(); }, get currentDate() { return new Date(); }, get profileAgeCreated() { return lazy.ProfileAge().then(times => times.created); }, get profileAgeReset() { return lazy.ProfileAge().then(times => times.reset); }, get usesFirefoxSync() { return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF); }, get isFxAEnabled() { return lazy.isFxAEnabled; }, get isFxASignedIn() { return new Promise(resolve => { if (!lazy.isFxAEnabled) { resolve(false); } if (Services.prefs.getStringPref(FXA_USERNAME_PREF, "")) { resolve(true); } lazy.fxAccounts .getSignedInUser() .then(data => resolve(!!data)) .catch(e => resolve(false)); }); }, get sync() { return { desktopDevices: lazy.clientsDevicesDesktop, mobileDevices: lazy.clientsDevicesMobile, totalDevices: lazy.syncNumClients, }; }, get xpinstallEnabled() { // This is needed for all add-on recommendations, to know if we allow xpi installs in the first place return lazy.isXPIInstallEnabled; }, get addonsInfo() { let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( Ci.nsIBackgroundTasks ); if (bts?.isBackgroundTaskMode) { return { addons: {}, isFullData: true }; } return lazy.AddonManager.getActiveAddons(["extension", "service"]).then( ({ addons, fullData }) => { const info = {}; for (const addon of addons) { info[addon.id] = { version: addon.version, type: addon.type, isSystem: addon.isSystem, isWebExtension: addon.isWebExtension, }; if (fullData) { Object.assign(info[addon.id], { name: addon.name, userDisabled: addon.userDisabled, installDate: addon.installDate, }); } } return { addons: info, isFullData: fullData }; } ); }, get searchEngines() { const NONE = { installed: [], current: "" }; let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( Ci.nsIBackgroundTasks ); if (bts?.isBackgroundTaskMode) { return Promise.resolve(NONE); } return new Promise(resolve => { // Note: calling init ensures this code is only executed after Search has been initialized Services.search .getAppProvidedEngines() .then(engines => { resolve({ current: Services.search.defaultEngine.identifier, installed: engines.map(engine => engine.identifier), }); }) .catch(() => resolve(NONE)); }); }, get isDefaultBrowser() { return QueryCache.getters.isDefaultBrowser.get().catch(() => null); }, get devToolsOpenedCount() { return lazy.devtoolsSelfXSSCount; }, get topFrecentSites() { return QueryCache.queries.TopFrecentSites.get().then(sites => sites.map(site => ({ url: site.url, host: new URL(site.url).hostname, frecency: site.frecency, lastVisitDate: site.lastVisitDate, })) ); }, get recentBookmarks() { return QueryCache.queries.RecentBookmarks.get(); }, get pinnedSites() { return NewTabUtils.pinnedLinks.links.map(site => site ? { url: site.url, host: new URL(site.url).hostname, searchTopSite: site.searchTopSite, } : {} ); }, get providerCohorts() { return lazy.ASRouterPreferences.providers.reduce((prev, current) => { prev[current.id] = current.cohort || ""; return prev; }, {}); }, get totalBookmarksCount() { return QueryCache.queries.TotalBookmarksCount.get(); }, get firefoxVersion() { return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10); }, get region() { return lazy.Region.home || ""; }, get needsUpdate() { return QueryCache.queries.CheckBrowserNeedsUpdate.get(); }, get hasPinnedTabs() { for (let win of Services.wm.getEnumerator("navigator:browser")) { if (win.closed || !win.ownerGlobal.gBrowser) { continue; } if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) { return true; } } return false; }, get hasAccessedFxAPanel() { return lazy.hasAccessedFxAPanel; }, get isWhatsNewPanelEnabled() { return lazy.isWhatsNewPanelEnabled; }, get userPrefs() { return { cfrFeatures: lazy.cfrFeaturesUserPref, cfrAddons: lazy.cfrAddonsUserPref, snippets: lazy.snippetsUserPref, }; }, get totalBlockedCount() { return lazy.TrackingDBService.sumAllEvents(); }, get blockedCountByType() { const idToTextMap = new Map([ [Ci.nsITrackingDBService.TRACKERS_ID, "trackerCount"], [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookieCount"], [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominerCount"], [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinterCount"], [Ci.nsITrackingDBService.SOCIAL_ID, "socialCount"], ]); const dateTo = new Date(); const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000); return lazy.TrackingDBService.getEventsByDateRange(dateFrom, dateTo).then( eventsByDate => { let totalEvents = {}; for (let blockedType of idToTextMap.values()) { totalEvents[blockedType] = 0; } return eventsByDate.reduce((acc, day) => { const type = day.getResultByName("type"); const count = day.getResultByName("count"); acc[idToTextMap.get(type)] = acc[idToTextMap.get(type)] + count; return acc; }, totalEvents); } ); }, get attachedFxAOAuthClients() { return this.usesFirefoxSync ? QueryCache.queries.ListAttachedOAuthClients.get() : []; }, get platformName() { return AppConstants.platform; }, get isChinaRepack() { return ( Services.prefs .getDefaultBranch(null) .getCharPref(DISTRIBUTION_ID_PREF, "default") === DISTRIBUTION_ID_CHINA_REPACK ); }, get userId() { return lazy.ClientEnvironment.userId; }, get profileRestartCount() { let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( Ci.nsIBackgroundTasks ); if (bts?.isBackgroundTaskMode) { return 0; } // Counter starts at 1 when a profile is created, substract 1 so the value // returned matches expectations return ( lazy.TelemetrySession.getMetadata("targeting").profileSubsessionCounter - 1 ); }, get homePageSettings() { const url = lazy.HomePage.get(); const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url); return { isWebExt, isCustomUrl, urls, isDefault: lazy.HomePage.isDefault, isLocked: lazy.HomePage.locked, }; }, get newtabSettings() { const url = lazy.AboutNewTab.newTabURL; const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url); return { isWebExt, isCustomUrl, isDefault: lazy.AboutNewTab.activityStreamEnabled, url: urls[0].url, host: urls[0].host, }; }, get activeNotifications() { let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( Ci.nsIBackgroundTasks ); if (bts?.isBackgroundTaskMode) { // This might need to hook into the alert service to enumerate relevant // persistent native notifications. return false; } let window = lazy.BrowserWindowTracker.getTopWindow(); // Technically this doesn't mean we have active notifications, // but because we use !activeNotifications to check for conflicts, this should return true if (!window) { return true; } if ( window.gURLBar?.view.isOpen || window.gNotificationBox?.currentNotification || window.gBrowser.getNotificationBox()?.currentNotification ) { return true; } return false; }, get isMajorUpgrade() { return lazy.BrowserHandler.majorUpgrade; }, get hasActiveEnterprisePolicies() { return Services.policies.status === Services.policies.ACTIVE; }, get userMonthlyActivity() { return QueryCache.queries.UserMonthlyActivity.get(); }, get doesAppNeedPin() { return QueryCache.getters.doesAppNeedPin.get(); }, get doesAppNeedPrivatePin() { return QueryCache.getters.doesAppNeedPrivatePin.get(); }, /** * Is this invocation running in background task mode? * * @return {boolean} `true` if running in background task mode. */ get isBackgroundTaskMode() { let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( Ci.nsIBackgroundTasks ); return !!bts?.isBackgroundTaskMode; }, /** * A non-empty task name if this invocation is running in background * task mode, or `null` if this invocation is not running in * background task mode. * * @return {string|null} background task name or `null`. */ get backgroundTaskName() { let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( Ci.nsIBackgroundTasks ); return bts?.backgroundTaskName(); }, get userPrefersReducedMotion() { let window = Services.appShell.hiddenDOMWindow; return window?.matchMedia("(prefers-reduced-motion: reduce)")?.matches; }, /** * Whether or not the user is in the Major Release 2022 holdback study. */ get inMr2022Holdback() { return ( lazy.NimbusFeatures.majorRelease2022.getVariable("onboarding") === false ); }, /** * The distribution id, if any. * @return {string} */ get distributionId() { return Services.prefs .getDefaultBranch(null) .getCharPref("distribution.id", ""); }, /** Where the Firefox View button is shown, if at all. * @return {string} container of the button if it is shown in the toolbar/overflow menu * @return {string} `null` if the button has been removed */ get fxViewButtonAreaType() { let button = lazy.CustomizableUI.getWidget("firefox-view-button"); return button.areaType; }, isDefaultHandler: { get html() { return QueryCache.getters.isDefaultHTMLHandler.get(); }, get pdf() { return QueryCache.getters.isDefaultPDFHandler.get(); }, }, get defaultPDFHandler() { return QueryCache.getters.defaultPDFHandler.get(); }, get creditCardsSaved() { return getAutofillRecords({ collectionName: "creditCards" }); }, get addressesSaved() { return getAutofillRecords({ collectionName: "addresses" }); }, /** * Has the user ever used the Migration Wizard to migrate bookmarks? * @return {boolean} `true` if bookmark migration has occurred. */ get hasMigratedBookmarks() { return lazy.hasMigratedBookmarks; }, /** * Has the user ever used the Migration Wizard to migrate history? * @return {boolean} `true` if history migration has occurred. */ get hasMigratedHistory() { return lazy.hasMigratedHistory; }, /** * Has the user ever used the Migration Wizard to migrate passwords? * @return {boolean} `true` if password migration has occurred. */ get hasMigratedPasswords() { return lazy.hasMigratedPasswords; }, /** * Returns true if the user is configured to use the embedded migration * wizard in about:welcome by having * "browser.migrate.content-modal.about-welcome-behavior" be equal to * "embedded". * @return {boolean} `true` if the embedded migration wizard is enabled. */ get useEmbeddedMigrationWizard() { return lazy.useEmbeddedMigrationWizard; }, /** * Whether the user installed Firefox via the RTAMO flow. * @return {boolean} `true` when RTAMO has been used to download Firefox, * `false` otherwise. */ get isRTAMO() { const { attributionData } = this; return ( attributionData?.source === "addons.mozilla.org" && !!decodeAttributionValue(attributionData?.content)?.startsWith("rta:") ); }, /** * Whether the user installed via the device migration flow. * @return {boolean} `true` when the link to download the browser was part * of guidance for device migration. `false` otherwise. */ get isDeviceMigration() { const { attributionData } = this; return attributionData?.campaign === "migration"; }, }; const ASRouterTargeting = { Environment: TargetingGetters, /** * Snapshot the current targeting environment. * * Asynchronous getters are handled. Getters that throw or reject * are ignored. * * @param {object} target - the environment to snapshot. * @return {object} snapshot of target with `environment` object and `version` * integer. */ async getEnvironmentSnapshot(target = ASRouterTargeting.Environment) { async function resolve(object) { if (typeof object === "object" && object !== null) { if (Array.isArray(object)) { return Promise.all(object.map(async item => resolve(await item))); } if (object instanceof Date) { return object; } // One promise for each named property. Label promises with property name. const promises = Object.keys(object).map(async key => { // Each promise needs to check if we're shutting down when it is evaluated. if (Services.startup.shuttingDown) { throw new Error( "shutting down, so not querying targeting environment" ); } const value = await resolve(await object[key]); return [key, value]; }); const resolved = {}; for (const result of await Promise.allSettled(promises)) { // Ignore properties that are rejected. if (result.status === "fulfilled") { const [key, value] = result.value; resolved[key] = value; } } return resolved; } return object; } const environment = await resolve(target); // Should we need to migrate in the future. const snapshot = { environment, version: 1 }; return snapshot; }, isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) { if (trigger.id !== candidateMessageTrigger.id) { return false; } else if ( !candidateMessageTrigger.params && !candidateMessageTrigger.patterns ) { return true; } if (!trigger.param) { return false; } return ( (candidateMessageTrigger.params && trigger.param.host && candidateMessageTrigger.params.includes(trigger.param.host)) || (candidateMessageTrigger.params && trigger.param.type && candidateMessageTrigger.params.filter(t => t === trigger.param.type) .length) || (candidateMessageTrigger.params && trigger.param.type && candidateMessageTrigger.params.filter( t => (t & trigger.param.type) === t ).length) || (candidateMessageTrigger.patterns && trigger.param.url && new MatchPatternSet(candidateMessageTrigger.patterns).matches( trigger.param.url )) ); }, /** * getCachedEvaluation - Return a cached jexl evaluation if available * * @param {string} targeting JEXL expression to lookup * @returns {obj|null} Object with value result or null if not available */ getCachedEvaluation(targeting) { if (jexlEvaluationCache.has(targeting)) { const { timestamp, value } = jexlEvaluationCache.get(targeting); if (Date.now() - timestamp <= CACHE_EXPIRATION) { return { value }; } jexlEvaluationCache.delete(targeting); } return null; }, /** * checkMessageTargeting - Checks is a message's targeting parameters are satisfied * * @param {*} message An AS router message * @param {obj} targetingContext a TargetingContext instance complete with eval environment * @param {func} onError A function to handle errors (takes two params; error, message) * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused. * @returns */ async checkMessageTargeting(message, targetingContext, onError, shouldCache) { lazy.ASRouterPreferences.console.debug( "in checkMessageTargeting, arguments = ", Array.from(arguments) // eslint-disable-line prefer-rest-params ); // If no targeting is specified, if (!message.targeting) { return true; } let result; try { if (shouldCache) { result = this.getCachedEvaluation(message.targeting); if (result) { return result.value; } } // Used to report the source of the targeting error in the case of // undesired events targetingContext.setTelemetrySource(message.id); result = await targetingContext.evalWithDefault(message.targeting); if (shouldCache) { jexlEvaluationCache.set(message.targeting, { timestamp: Date.now(), value: result, }); } } catch (error) { if (onError) { onError(error, message); } console.error(error); result = false; } return result; }, _isMessageMatch( message, trigger, targetingContext, onError, shouldCache = false ) { return ( message && (trigger ? this.isTriggerMatch(trigger, message.trigger) : !message.trigger) && // If a trigger expression was passed to this function, the message should match it. // Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time) this.checkMessageTargeting( message, targetingContext, onError, shouldCache ) ); }, /** * findMatchingMessage - Given an array of messages, returns one message * whos targeting expression evaluates to true * * @param {Array} messages An array of AS router messages * @param {trigger} string A trigger expression if a message for that trigger is desired * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above. * @param {func} onError A function to handle errors (takes two params; error, message) * @param {func} ordered An optional param when true sort message by order specified in message * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused. * @param {boolean} returnAll Should we return all matching messages, not just the first one found. * @returns {obj|Array} If returnAll is false, a single message. If returnAll is true, an array of messages. */ async findMatchingMessage({ messages, trigger = {}, context = {}, onError, ordered = false, shouldCache = false, returnAll = false, }) { const sortedMessages = getSortedMessages(messages, { ordered }); lazy.ASRouterPreferences.console.debug( "in findMatchingMessage, sortedMessages = ", sortedMessages ); const matching = returnAll ? [] : null; const targetingContext = new lazy.TargetingContext( lazy.TargetingContext.combineContexts( context, this.Environment, trigger.context || {} ) ); const isMatch = candidate => this._isMessageMatch( candidate, trigger, targetingContext, onError, shouldCache ); for (const candidate of sortedMessages) { if (await isMatch(candidate)) { // If not returnAll, we should return the first message we find that matches. if (!returnAll) { return candidate; } matching.push(candidate); } } return matching; }, }; const EXPORTED_SYMBOLS = [ "ASRouterTargeting", "QueryCache", "CachedTargetingGetter", "getSortedMessages", ];