forked from mirrors/gecko-dev
		
	 677b4036a4
			
		
	
	
		677b4036a4
		
	
	
	
	
		
			
			This feature is no longer intended for experimentation and some of the things it controls are no longer in product. Differential Revision: https://phabricator.services.mozilla.com/D204514
		
			
				
	
	
		
			1302 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1302 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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";
 | |
| 
 | |
| // We use importESModule here instead of static import so that
 | |
| // the Karma test environment won't choke on this module. This
 | |
| // is because the Karma test environment already stubs out
 | |
| // XPCOMUtils, AppConstants, NewTabUtils and ShellService, and
 | |
| // overrides importESModule to be a no-op (which can't be done
 | |
| // for a static import statement).
 | |
| 
 | |
| // eslint-disable-next-line mozilla/use-static-import
 | |
| const { XPCOMUtils } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/XPCOMUtils.sys.mjs"
 | |
| );
 | |
| 
 | |
| // eslint-disable-next-line mozilla/use-static-import
 | |
| const { AppConstants } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/AppConstants.sys.mjs"
 | |
| );
 | |
| 
 | |
| // eslint-disable-next-line mozilla/use-static-import
 | |
| const { NewTabUtils } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/NewTabUtils.sys.mjs"
 | |
| );
 | |
| 
 | |
| // eslint-disable-next-line mozilla/use-static-import
 | |
| const { ShellService } = ChromeUtils.importESModule(
 | |
|   "resource:///modules/ShellService.sys.mjs"
 | |
| );
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
 | |
|   AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
 | |
|   ASRouterPreferences:
 | |
|     "resource:///modules/asrouter/ASRouterPreferences.sys.mjs",
 | |
|   AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
 | |
|   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
 | |
|   ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
 | |
|   CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
 | |
|   HomePage: "resource:///modules/HomePage.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",
 | |
|   WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
 | |
| });
 | |
| 
 | |
| ChromeUtils.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,
 | |
|   "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,
 | |
|   "hasMigratedBookmarks",
 | |
|   "browser.migrate.interactions.bookmarks",
 | |
|   false
 | |
| );
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "hasMigratedCSVPasswords",
 | |
|   "browser.migrate.interactions.csvpasswords",
 | |
|   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
 | |
|  */
 | |
| export 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) {
 | |
|         lazy.ASRouterPreferences.console.error(
 | |
|           "CheckBrowserNeedsUpdate failed :>> ",
 | |
|           result.request
 | |
|         );
 | |
|         return false;
 | |
|       }
 | |
|       checker._value = !!result.updates.length;
 | |
|       return checker._value;
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   return checker;
 | |
| }
 | |
| 
 | |
| export 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<Message>} messages
 | |
|  * @param {{}} options
 | |
|  * @param {boolean} options.ordered - Should .order be used instead of random weighted sorting?
 | |
|  * @returns {Array<Message>}
 | |
|  */
 | |
| export 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<number>} 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?.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(() => 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 userPrefs() {
 | |
|     return {
 | |
|       cfrFeatures: lazy.cfrFeaturesUserPref,
 | |
|       cfrAddons: lazy.cfrAddonsUserPref,
 | |
|     };
 | |
|   },
 | |
|   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();
 | |
|   },
 | |
| 
 | |
|   get launchOnLoginEnabled() {
 | |
|     if (AppConstants.platform !== "win") {
 | |
|       return false;
 | |
|     }
 | |
|     return lazy.WindowsLaunchOnLogin.getLaunchOnLoginEnabled();
 | |
|   },
 | |
| 
 | |
|   get isMSIX() {
 | |
|     if (AppConstants.platform !== "win") {
 | |
|       return false;
 | |
|     }
 | |
|     // While we can write registry keys using external programs, we have no
 | |
|     // way of cleanup on uninstall. If we are on an MSIX build
 | |
|     // launch on login should never be enabled.
 | |
|     // Default to false so that the feature isn't unnecessarily
 | |
|     // disabled.
 | |
|     // See Bug 1888263.
 | |
|     return Services.sysinfo.getProperty("hasWinPackageId", false);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 passwords from
 | |
|    * a CSV file?
 | |
|    * @return {boolean} `true` if CSV passwords have been imported via the
 | |
|    *   migration wizard.
 | |
|    */
 | |
|   get hasMigratedCSVPasswords() {
 | |
|     return lazy.hasMigratedCSVPasswords;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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";
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The values of the height and width available to the browser to display
 | |
|    * web content. The available height and width are each calculated taking
 | |
|    * into account the presence of menu bars, docks, and other similar OS elements
 | |
|    * @returns {Object} resolution The resolution object containing width and height
 | |
|    * @returns {string} resolution.width The available width of the primary monitor
 | |
|    * @returns {string} resolution.height The available height of the primary monitor
 | |
|    */
 | |
|   get primaryResolution() {
 | |
|     // Using hidden dom window ensures that we have a window object
 | |
|     // to grab a screen from in certain edge cases such as targeting evaluation
 | |
|     // during first startup before the browser is available, and in MacOS
 | |
|     let window = Services.appShell.hiddenDOMWindow;
 | |
|     return {
 | |
|       width: window?.screen.availWidth,
 | |
|       height: window?.screen.availHeight,
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   get archBits() {
 | |
|     let bits = null;
 | |
|     try {
 | |
|       bits = Services.sysinfo.getProperty("archbits", null);
 | |
|     } catch (_e) {
 | |
|       // getProperty can throw if the memsize does not exist
 | |
|     }
 | |
|     if (bits) {
 | |
|       bits = Number(bits);
 | |
|     }
 | |
|     return bits;
 | |
|   },
 | |
| 
 | |
|   get memoryMB() {
 | |
|     let memory = null;
 | |
|     try {
 | |
|       memory = Services.sysinfo.getProperty("memsize", null);
 | |
|     } catch (_e) {
 | |
|       // getProperty can throw if the memsize does not exist
 | |
|     }
 | |
|     if (memory) {
 | |
|       memory = Number(memory) / 1024 / 1024;
 | |
|     }
 | |
|     return memory;
 | |
|   },
 | |
| };
 | |
| 
 | |
| export const ASRouterTargeting = {
 | |
|   Environment: TargetingGetters,
 | |
| 
 | |
|   /**
 | |
|    * Snapshot the current targeting environment.
 | |
|    *
 | |
|    * Asynchronous getters are handled.  Getters that throw or reject
 | |
|    * are ignored.
 | |
|    *
 | |
|    * Leftward (earlier) targets supercede rightward (later) targets, just like
 | |
|    * `TargetingContext.combineContexts`.
 | |
|    *
 | |
|    * @param {object} options - object containing:
 | |
|    * @param {Array<object>|null} options.targets -
 | |
|    *        targeting environments to snapshot; (default: `[ASRouterTargeting.Environment]`)
 | |
|    * @return {object} snapshot of target with `environment` object and `version` integer.
 | |
|    */
 | |
|   async getEnvironmentSnapshot({
 | |
|     targets = [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;
 | |
|     }
 | |
| 
 | |
|     // We would like to use `TargetingContext.combineContexts`, but `Proxy`
 | |
|     // instances complicate iterating with `Object.keys`.  Instead, merge by
 | |
|     // hand after resolving.
 | |
|     const environment = {};
 | |
|     for (let target of targets.toReversed()) {
 | |
|       Object.assign(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<Message>} 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<Message>} 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;
 | |
|   },
 | |
| };
 |