Bug 1863867 - Add expriment targeting based on archiecture bits r=barret
[gecko.git] / browser / components / newtab / lib / ASRouterTargeting.jsm
blobdd2242890163edc417a9cc3a48c0f16c56963b7a
1 /* This Source Code Form is subject to the terms of the Mozilla PublicddonMa
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const FXA_ENABLED_PREF = "identity.fxaccounts.enabled";
6 const DISTRIBUTION_ID_PREF = "distribution.id";
7 const DISTRIBUTION_ID_CHINA_REPACK = "MozillaOnline";
9 const { XPCOMUtils } = ChromeUtils.importESModule(
10   "resource://gre/modules/XPCOMUtils.sys.mjs"
12 const { AppConstants } = ChromeUtils.importESModule(
13   "resource://gre/modules/AppConstants.sys.mjs"
15 const { NewTabUtils } = ChromeUtils.importESModule(
16   "resource://gre/modules/NewTabUtils.sys.mjs"
18 const { ShellService } = ChromeUtils.importESModule(
19   "resource:///modules/ShellService.sys.mjs"
22 const lazy = {};
24 ChromeUtils.defineESModuleGetters(lazy, {
25   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
26   AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
27   AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
28   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
29   ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
30   CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
31   HomePage: "resource:///modules/HomePage.sys.mjs",
32   NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
33   ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
34   Region: "resource://gre/modules/Region.sys.mjs",
35   TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs",
36   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
37   TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
38 });
40 XPCOMUtils.defineLazyModuleGetters(lazy, {
41   ASRouterPreferences: "resource://activity-stream/lib/ASRouterPreferences.jsm",
42 });
44 XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
45   return ChromeUtils.importESModule(
46     "resource://gre/modules/FxAccounts.sys.mjs"
47   ).getFxAccountsSingleton();
48 });
50 XPCOMUtils.defineLazyPreferenceGetter(
51   lazy,
52   "cfrFeaturesUserPref",
53   "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
54   true
56 XPCOMUtils.defineLazyPreferenceGetter(
57   lazy,
58   "cfrAddonsUserPref",
59   "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
60   true
62 XPCOMUtils.defineLazyPreferenceGetter(
63   lazy,
64   "isWhatsNewPanelEnabled",
65   "browser.messaging-system.whatsNewPanel.enabled",
66   false
68 XPCOMUtils.defineLazyPreferenceGetter(
69   lazy,
70   "hasAccessedFxAPanel",
71   "identity.fxaccounts.toolbar.accessed",
72   false
74 XPCOMUtils.defineLazyPreferenceGetter(
75   lazy,
76   "clientsDevicesDesktop",
77   "services.sync.clients.devices.desktop",
78   0
80 XPCOMUtils.defineLazyPreferenceGetter(
81   lazy,
82   "clientsDevicesMobile",
83   "services.sync.clients.devices.mobile",
84   0
86 XPCOMUtils.defineLazyPreferenceGetter(
87   lazy,
88   "syncNumClients",
89   "services.sync.numClients",
90   0
92 XPCOMUtils.defineLazyPreferenceGetter(
93   lazy,
94   "devtoolsSelfXSSCount",
95   "devtools.selfxss.count",
96   0
98 XPCOMUtils.defineLazyPreferenceGetter(
99   lazy,
100   "isFxAEnabled",
101   FXA_ENABLED_PREF,
102   true
104 XPCOMUtils.defineLazyPreferenceGetter(
105   lazy,
106   "isXPIInstallEnabled",
107   "xpinstall.enabled",
108   true
110 XPCOMUtils.defineLazyPreferenceGetter(
111   lazy,
112   "snippetsUserPref",
113   "browser.newtabpage.activity-stream.feeds.snippets",
114   false
116 XPCOMUtils.defineLazyPreferenceGetter(
117   lazy,
118   "hasMigratedBookmarks",
119   "browser.migrate.interactions.bookmarks",
120   false
122 XPCOMUtils.defineLazyPreferenceGetter(
123   lazy,
124   "hasMigratedCSVPasswords",
125   "browser.migrate.interactions.csvpasswords",
126   false
128 XPCOMUtils.defineLazyPreferenceGetter(
129   lazy,
130   "hasMigratedHistory",
131   "browser.migrate.interactions.history",
132   false
134 XPCOMUtils.defineLazyPreferenceGetter(
135   lazy,
136   "hasMigratedPasswords",
137   "browser.migrate.interactions.passwords",
138   false
140 XPCOMUtils.defineLazyPreferenceGetter(
141   lazy,
142   "useEmbeddedMigrationWizard",
143   "browser.migrate.content-modal.about-welcome-behavior",
144   "default",
145   null,
146   behaviorString => {
147     return behaviorString === "embedded";
148   }
151 XPCOMUtils.defineLazyServiceGetters(lazy, {
152   AUS: ["@mozilla.org/updates/update-service;1", "nsIApplicationUpdateService"],
153   BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
154   TrackingDBService: [
155     "@mozilla.org/tracking-db-service;1",
156     "nsITrackingDBService",
157   ],
158   UpdateCheckSvc: ["@mozilla.org/updates/update-checker;1", "nsIUpdateChecker"],
161 const FXA_USERNAME_PREF = "services.sync.username";
163 const { activityStreamProvider: asProvider } = NewTabUtils;
165 const FXA_ATTACHED_CLIENTS_UPDATE_INTERVAL = 4 * 60 * 60 * 1000; // Four hours
166 const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
167 const FRECENT_SITES_IGNORE_BLOCKED = false;
168 const FRECENT_SITES_NUM_ITEMS = 25;
169 const FRECENT_SITES_MIN_FRECENCY = 100;
171 const CACHE_EXPIRATION = 5 * 60 * 1000;
172 const jexlEvaluationCache = new Map();
175  * CachedTargetingGetter
176  * @param property {string} Name of the method
177  * @param options {any=} Options passed to the method
178  * @param updateInterval {number?} Update interval for query. Defaults to FRECENT_SITES_UPDATE_INTERVAL
179  */
180 function CachedTargetingGetter(
181   property,
182   options = null,
183   updateInterval = FRECENT_SITES_UPDATE_INTERVAL,
184   getter = asProvider
185 ) {
186   return {
187     _lastUpdated: 0,
188     _value: null,
189     // For testing
190     expire() {
191       this._lastUpdated = 0;
192       this._value = null;
193     },
194     async get() {
195       const now = Date.now();
196       if (now - this._lastUpdated >= updateInterval) {
197         this._value = await getter[property](options);
198         this._lastUpdated = now;
199       }
200       return this._value;
201     },
202   };
205 function CacheListAttachedOAuthClients() {
206   return {
207     _lastUpdated: 0,
208     _value: null,
209     expire() {
210       this._lastUpdated = 0;
211       this._value = null;
212     },
213     get() {
214       const now = Date.now();
215       if (now - this._lastUpdated >= FXA_ATTACHED_CLIENTS_UPDATE_INTERVAL) {
216         this._value = new Promise(resolve => {
217           lazy.fxAccounts
218             .listAttachedOAuthClients()
219             .then(clients => {
220               resolve(clients);
221             })
222             .catch(() => resolve([]));
223         });
224         this._lastUpdated = now;
225       }
226       return this._value;
227     },
228   };
231 function CheckBrowserNeedsUpdate(
232   updateInterval = FRECENT_SITES_UPDATE_INTERVAL
233 ) {
234   const checker = {
235     _lastUpdated: 0,
236     _value: null,
237     // For testing. Avoid update check network call.
238     setUp(value) {
239       this._lastUpdated = Date.now();
240       this._value = value;
241     },
242     expire() {
243       this._lastUpdated = 0;
244       this._value = null;
245     },
246     async get() {
247       const now = Date.now();
248       if (
249         !AppConstants.MOZ_UPDATER ||
250         now - this._lastUpdated < updateInterval
251       ) {
252         return this._value;
253       }
254       if (!lazy.AUS.canCheckForUpdates) {
255         return false;
256       }
257       this._lastUpdated = now;
258       let check = lazy.UpdateCheckSvc.checkForUpdates(
259         lazy.UpdateCheckSvc.FOREGROUND_CHECK
260       );
261       let result = await check.result;
262       if (!result.succeeded) {
263         lazy.ASRouterPreferences.console.error(
264           "CheckBrowserNeedsUpdate failed :>> ",
265           result.request
266         );
267         return false;
268       }
269       checker._value = !!result.updates.length;
270       return checker._value;
271     },
272   };
274   return checker;
277 const QueryCache = {
278   expireAll() {
279     Object.keys(this.queries).forEach(query => {
280       this.queries[query].expire();
281     });
282     Object.keys(this.getters).forEach(key => {
283       this.getters[key].expire();
284     });
285   },
286   queries: {
287     TopFrecentSites: new CachedTargetingGetter("getTopFrecentSites", {
288       ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
289       numItems: FRECENT_SITES_NUM_ITEMS,
290       topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
291       onePerDomain: true,
292       includeFavicon: false,
293     }),
294     TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"),
295     CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(),
296     RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"),
297     ListAttachedOAuthClients: new CacheListAttachedOAuthClients(),
298     UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"),
299   },
300   getters: {
301     doesAppNeedPin: new CachedTargetingGetter(
302       "doesAppNeedPin",
303       null,
304       FRECENT_SITES_UPDATE_INTERVAL,
305       ShellService
306     ),
307     doesAppNeedPrivatePin: new CachedTargetingGetter(
308       "doesAppNeedPin",
309       true,
310       FRECENT_SITES_UPDATE_INTERVAL,
311       ShellService
312     ),
313     isDefaultBrowser: new CachedTargetingGetter(
314       "isDefaultBrowser",
315       null,
316       FRECENT_SITES_UPDATE_INTERVAL,
317       ShellService
318     ),
319     currentThemes: new CachedTargetingGetter(
320       "getAddonsByTypes",
321       ["theme"],
322       FRECENT_SITES_UPDATE_INTERVAL,
323       lazy.AddonManager // eslint-disable-line mozilla/valid-lazy
324     ),
325     isDefaultHTMLHandler: new CachedTargetingGetter(
326       "isDefaultHandlerFor",
327       [".html"],
328       FRECENT_SITES_UPDATE_INTERVAL,
329       ShellService
330     ),
331     isDefaultPDFHandler: new CachedTargetingGetter(
332       "isDefaultHandlerFor",
333       [".pdf"],
334       FRECENT_SITES_UPDATE_INTERVAL,
335       ShellService
336     ),
337     defaultPDFHandler: new CachedTargetingGetter(
338       "getDefaultPDFHandler",
339       null,
340       FRECENT_SITES_UPDATE_INTERVAL,
341       ShellService
342     ),
343   },
347  * sortMessagesByWeightedRank
349  * Each message has an associated weight, which is guaranteed to be strictly
350  * positive. Sort the messages so that higher weighted messages are more likely
351  * to come first.
353  * Specifically, sort them so that the probability of message x_1 with weight
354  * w_1 appearing before message x_2 with weight w_2 is (w_1 / (w_1 + w_2)).
356  * This is equivalent to requiring that x_1 appearing before x_2 is (w_1 / w_2)
357  * "times" as likely as x_2 appearing before x_1.
359  * See Bug 1484996, Comment 2 for a justification of the method.
361  * @param {Array} messages - A non-empty array of messages to sort, all with
362  *                           strictly positive weights
363  * @returns the sorted array
364  */
365 function sortMessagesByWeightedRank(messages) {
366   return messages
367     .map(message => ({
368       message,
369       rank: Math.pow(Math.random(), 1 / message.weight),
370     }))
371     .sort((a, b) => b.rank - a.rank)
372     .map(({ message }) => message);
376  * getSortedMessages - Given an array of Messages, applies sorting and filtering rules
377  *                     in expected order.
379  * @param {Array<Message>} messages
380  * @param {{}} options
381  * @param {boolean} options.ordered - Should .order be used instead of random weighted sorting?
382  * @returns {Array<Message>}
383  */
384 function getSortedMessages(messages, options = {}) {
385   let { ordered } = { ordered: false, ...options };
386   let result = messages;
388   if (!ordered) {
389     result = sortMessagesByWeightedRank(result);
390   }
392   result.sort((a, b) => {
393     // Next, sort by priority
394     if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) {
395       return -1;
396     }
397     if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) {
398       return 1;
399     }
401     // Sort messages with targeting expressions higher than those with none
402     if (a.targeting && !b.targeting) {
403       return -1;
404     }
405     if (!a.targeting && b.targeting) {
406       return 1;
407     }
409     // Next, sort by order *ascending* if ordered = true
410     if (ordered) {
411       if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) {
412         return 1;
413       }
414       if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) {
415         return -1;
416       }
417     }
419     return 0;
420   });
422   return result;
426  * parseAboutPageURL - Parse a URL string retrieved from about:home and about:new, returns
427  *                    its type (web extenstion or custom url) and the parsed url(s)
429  * @param {string} url - A URL string for home page or newtab page
430  * @returns {Object} {
431  *   isWebExt: boolean,
432  *   isCustomUrl: boolean,
433  *   urls: Array<{url: string, host: string}>
434  * }
435  */
436 function parseAboutPageURL(url) {
437   let ret = {
438     isWebExt: false,
439     isCustomUrl: false,
440     urls: [],
441   };
442   if (url.startsWith("moz-extension://")) {
443     ret.isWebExt = true;
444     ret.urls.push({ url, host: "" });
445   } else {
446     // The home page URL could be either a single URL or a list of "|" separated URLs.
447     // Note that it should work with "about:home" and "about:blank", in which case the
448     // "host" is set as an empty string.
449     for (const _url of url.split("|")) {
450       if (!["about:home", "about:newtab", "about:blank"].includes(_url)) {
451         ret.isCustomUrl = true;
452       }
453       try {
454         const parsedURL = new URL(_url);
455         const host = parsedURL.hostname.replace(/^www\./i, "");
456         ret.urls.push({ url: _url, host });
457       } catch (e) {}
458     }
459     // If URL parsing failed, just return the given url with an empty host
460     if (!ret.urls.length) {
461       ret.urls.push({ url, host: "" });
462     }
463   }
465   return ret;
469  * Get the number of records in autofill storage, e.g. credit cards/addresses.
471  * @param  {Object} [data]
472  * @param  {string} [data.collectionName]
473  *         The name used to specify which collection to retrieve records.
474  * @param  {string} [data.searchString]
475  *         The typed string for filtering out the matched records.
476  * @param  {string} [data.info]
477  *         The input autocomplete property's information.
478  * @returns {Promise<number>} The number of matched records.
479  * @see FormAutofillParent._getRecords
480  */
481 async function getAutofillRecords(data) {
482   let actor;
483   try {
484     const win = Services.wm.getMostRecentBrowserWindow();
485     actor =
486       win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
487         "FormAutofill"
488       );
489   } catch (error) {
490     // If the actor is not available, we can't get the records. We could import
491     // the records directly from FormAutofillStorage to avoid the messiness of
492     // JSActors, but that would import a lot of code for a targeting attribute.
493     return 0;
494   }
495   let records = await actor?.receiveMessage({
496     name: "FormAutofill:GetRecords",
497     data,
498   });
499   return records?.records?.length ?? 0;
502 // Attribution data can be encoded multiple times so we need this function to
503 // get a cleartext value.
504 function decodeAttributionValue(value) {
505   if (!value) {
506     return null;
507   }
509   let decodedValue = value;
511   while (decodedValue.includes("%")) {
512     try {
513       const result = decodeURIComponent(decodedValue);
514       if (result === decodedValue) {
515         break;
516       }
517       decodedValue = result;
518     } catch (e) {
519       break;
520     }
521   }
523   return decodedValue;
526 const TargetingGetters = {
527   get locale() {
528     return Services.locale.appLocaleAsBCP47;
529   },
530   get localeLanguageCode() {
531     return (
532       Services.locale.appLocaleAsBCP47 &&
533       Services.locale.appLocaleAsBCP47.substr(0, 2)
534     );
535   },
536   get browserSettings() {
537     const { settings } = lazy.TelemetryEnvironment.currentEnvironment;
538     return {
539       update: settings.update,
540     };
541   },
542   get attributionData() {
543     // Attribution is determined at startup - so we can use the cached attribution at this point
544     return lazy.AttributionCode.getCachedAttributionData();
545   },
546   get currentDate() {
547     return new Date();
548   },
549   get profileAgeCreated() {
550     return lazy.ProfileAge().then(times => times.created);
551   },
552   get profileAgeReset() {
553     return lazy.ProfileAge().then(times => times.reset);
554   },
555   get usesFirefoxSync() {
556     return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
557   },
558   get isFxAEnabled() {
559     return lazy.isFxAEnabled;
560   },
561   get isFxASignedIn() {
562     return new Promise(resolve => {
563       if (!lazy.isFxAEnabled) {
564         resolve(false);
565       }
566       if (Services.prefs.getStringPref(FXA_USERNAME_PREF, "")) {
567         resolve(true);
568       }
569       lazy.fxAccounts
570         .getSignedInUser()
571         .then(data => resolve(!!data))
572         .catch(e => resolve(false));
573     });
574   },
575   get sync() {
576     return {
577       desktopDevices: lazy.clientsDevicesDesktop,
578       mobileDevices: lazy.clientsDevicesMobile,
579       totalDevices: lazy.syncNumClients,
580     };
581   },
582   get xpinstallEnabled() {
583     // This is needed for all add-on recommendations, to know if we allow xpi installs in the first place
584     return lazy.isXPIInstallEnabled;
585   },
586   get addonsInfo() {
587     let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
588       Ci.nsIBackgroundTasks
589     );
590     if (bts?.isBackgroundTaskMode) {
591       return { addons: {}, isFullData: true };
592     }
594     return lazy.AddonManager.getActiveAddons(["extension", "service"]).then(
595       ({ addons, fullData }) => {
596         const info = {};
597         for (const addon of addons) {
598           info[addon.id] = {
599             version: addon.version,
600             type: addon.type,
601             isSystem: addon.isSystem,
602             isWebExtension: addon.isWebExtension,
603           };
604           if (fullData) {
605             Object.assign(info[addon.id], {
606               name: addon.name,
607               userDisabled: addon.userDisabled,
608               installDate: addon.installDate,
609             });
610           }
611         }
612         return { addons: info, isFullData: fullData };
613       }
614     );
615   },
616   get searchEngines() {
617     const NONE = { installed: [], current: "" };
618     let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
619       Ci.nsIBackgroundTasks
620     );
621     if (bts?.isBackgroundTaskMode) {
622       return Promise.resolve(NONE);
623     }
624     return new Promise(resolve => {
625       // Note: calling init ensures this code is only executed after Search has been initialized
626       Services.search
627         .getAppProvidedEngines()
628         .then(engines => {
629           resolve({
630             current: Services.search.defaultEngine.identifier,
631             installed: engines.map(engine => engine.identifier),
632           });
633         })
634         .catch(() => resolve(NONE));
635     });
636   },
637   get isDefaultBrowser() {
638     return QueryCache.getters.isDefaultBrowser.get().catch(() => null);
639   },
640   get devToolsOpenedCount() {
641     return lazy.devtoolsSelfXSSCount;
642   },
643   get topFrecentSites() {
644     return QueryCache.queries.TopFrecentSites.get().then(sites =>
645       sites.map(site => ({
646         url: site.url,
647         host: new URL(site.url).hostname,
648         frecency: site.frecency,
649         lastVisitDate: site.lastVisitDate,
650       }))
651     );
652   },
653   get recentBookmarks() {
654     return QueryCache.queries.RecentBookmarks.get();
655   },
656   get pinnedSites() {
657     return NewTabUtils.pinnedLinks.links.map(site =>
658       site
659         ? {
660             url: site.url,
661             host: new URL(site.url).hostname,
662             searchTopSite: site.searchTopSite,
663           }
664         : {}
665     );
666   },
667   get providerCohorts() {
668     return lazy.ASRouterPreferences.providers.reduce((prev, current) => {
669       prev[current.id] = current.cohort || "";
670       return prev;
671     }, {});
672   },
673   get totalBookmarksCount() {
674     return QueryCache.queries.TotalBookmarksCount.get();
675   },
676   get firefoxVersion() {
677     return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10);
678   },
679   get region() {
680     return lazy.Region.home || "";
681   },
682   get needsUpdate() {
683     return QueryCache.queries.CheckBrowserNeedsUpdate.get();
684   },
685   get hasPinnedTabs() {
686     for (let win of Services.wm.getEnumerator("navigator:browser")) {
687       if (win.closed || !win.ownerGlobal.gBrowser) {
688         continue;
689       }
690       if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) {
691         return true;
692       }
693     }
695     return false;
696   },
697   get hasAccessedFxAPanel() {
698     return lazy.hasAccessedFxAPanel;
699   },
700   get isWhatsNewPanelEnabled() {
701     return lazy.isWhatsNewPanelEnabled;
702   },
703   get userPrefs() {
704     return {
705       cfrFeatures: lazy.cfrFeaturesUserPref,
706       cfrAddons: lazy.cfrAddonsUserPref,
707       snippets: lazy.snippetsUserPref,
708     };
709   },
710   get totalBlockedCount() {
711     return lazy.TrackingDBService.sumAllEvents();
712   },
713   get blockedCountByType() {
714     const idToTextMap = new Map([
715       [Ci.nsITrackingDBService.TRACKERS_ID, "trackerCount"],
716       [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookieCount"],
717       [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominerCount"],
718       [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinterCount"],
719       [Ci.nsITrackingDBService.SOCIAL_ID, "socialCount"],
720     ]);
722     const dateTo = new Date();
723     const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000);
724     return lazy.TrackingDBService.getEventsByDateRange(dateFrom, dateTo).then(
725       eventsByDate => {
726         let totalEvents = {};
727         for (let blockedType of idToTextMap.values()) {
728           totalEvents[blockedType] = 0;
729         }
731         return eventsByDate.reduce((acc, day) => {
732           const type = day.getResultByName("type");
733           const count = day.getResultByName("count");
734           acc[idToTextMap.get(type)] = acc[idToTextMap.get(type)] + count;
735           return acc;
736         }, totalEvents);
737       }
738     );
739   },
740   get attachedFxAOAuthClients() {
741     return this.usesFirefoxSync
742       ? QueryCache.queries.ListAttachedOAuthClients.get()
743       : [];
744   },
745   get platformName() {
746     return AppConstants.platform;
747   },
748   get isChinaRepack() {
749     return (
750       Services.prefs
751         .getDefaultBranch(null)
752         .getCharPref(DISTRIBUTION_ID_PREF, "default") ===
753       DISTRIBUTION_ID_CHINA_REPACK
754     );
755   },
756   get userId() {
757     return lazy.ClientEnvironment.userId;
758   },
759   get profileRestartCount() {
760     let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
761       Ci.nsIBackgroundTasks
762     );
763     if (bts?.isBackgroundTaskMode) {
764       return 0;
765     }
766     // Counter starts at 1 when a profile is created, substract 1 so the value
767     // returned matches expectations
768     return (
769       lazy.TelemetrySession.getMetadata("targeting").profileSubsessionCounter -
770       1
771     );
772   },
773   get homePageSettings() {
774     const url = lazy.HomePage.get();
775     const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
777     return {
778       isWebExt,
779       isCustomUrl,
780       urls,
781       isDefault: lazy.HomePage.isDefault,
782       isLocked: lazy.HomePage.locked,
783     };
784   },
785   get newtabSettings() {
786     const url = lazy.AboutNewTab.newTabURL;
787     const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
789     return {
790       isWebExt,
791       isCustomUrl,
792       isDefault: lazy.AboutNewTab.activityStreamEnabled,
793       url: urls[0].url,
794       host: urls[0].host,
795     };
796   },
797   get activeNotifications() {
798     let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
799       Ci.nsIBackgroundTasks
800     );
801     if (bts?.isBackgroundTaskMode) {
802       // This might need to hook into the alert service to enumerate relevant
803       // persistent native notifications.
804       return false;
805     }
807     let window = lazy.BrowserWindowTracker.getTopWindow();
809     // Technically this doesn't mean we have active notifications,
810     // but because we use !activeNotifications to check for conflicts, this should return true
811     if (!window) {
812       return true;
813     }
815     if (
816       window.gURLBar?.view.isOpen ||
817       window.gNotificationBox?.currentNotification ||
818       window.gBrowser.getNotificationBox()?.currentNotification
819     ) {
820       return true;
821     }
823     return false;
824   },
826   get isMajorUpgrade() {
827     return lazy.BrowserHandler.majorUpgrade;
828   },
830   get hasActiveEnterprisePolicies() {
831     return Services.policies.status === Services.policies.ACTIVE;
832   },
834   get userMonthlyActivity() {
835     return QueryCache.queries.UserMonthlyActivity.get();
836   },
838   get doesAppNeedPin() {
839     return QueryCache.getters.doesAppNeedPin.get();
840   },
842   get doesAppNeedPrivatePin() {
843     return QueryCache.getters.doesAppNeedPrivatePin.get();
844   },
846   /**
847    * Is this invocation running in background task mode?
848    *
849    * @return {boolean} `true` if running in background task mode.
850    */
851   get isBackgroundTaskMode() {
852     let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
853       Ci.nsIBackgroundTasks
854     );
855     return !!bts?.isBackgroundTaskMode;
856   },
858   /**
859    * A non-empty task name if this invocation is running in background
860    * task mode, or `null` if this invocation is not running in
861    * background task mode.
862    *
863    * @return {string|null} background task name or `null`.
864    */
865   get backgroundTaskName() {
866     let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
867       Ci.nsIBackgroundTasks
868     );
869     return bts?.backgroundTaskName();
870   },
872   get userPrefersReducedMotion() {
873     let window = Services.appShell.hiddenDOMWindow;
874     return window?.matchMedia("(prefers-reduced-motion: reduce)")?.matches;
875   },
877   /**
878    * Whether or not the user is in the Major Release 2022 holdback study.
879    */
880   get inMr2022Holdback() {
881     return (
882       lazy.NimbusFeatures.majorRelease2022.getVariable("onboarding") === false
883     );
884   },
886   /**
887    * The distribution id, if any.
888    * @return {string}
889    */
890   get distributionId() {
891     return Services.prefs
892       .getDefaultBranch(null)
893       .getCharPref("distribution.id", "");
894   },
896   /** Where the Firefox View button is shown, if at all.
897    * @return {string} container of the button if it is shown in the toolbar/overflow menu
898    * @return {string} `null` if the button has been removed
899    */
900   get fxViewButtonAreaType() {
901     let button = lazy.CustomizableUI.getWidget("firefox-view-button");
902     return button.areaType;
903   },
905   isDefaultHandler: {
906     get html() {
907       return QueryCache.getters.isDefaultHTMLHandler.get();
908     },
909     get pdf() {
910       return QueryCache.getters.isDefaultPDFHandler.get();
911     },
912   },
914   get defaultPDFHandler() {
915     return QueryCache.getters.defaultPDFHandler.get();
916   },
918   get creditCardsSaved() {
919     return getAutofillRecords({ collectionName: "creditCards" });
920   },
922   get addressesSaved() {
923     return getAutofillRecords({ collectionName: "addresses" });
924   },
926   /**
927    * Has the user ever used the Migration Wizard to migrate bookmarks?
928    * @return {boolean} `true` if bookmark migration has occurred.
929    */
930   get hasMigratedBookmarks() {
931     return lazy.hasMigratedBookmarks;
932   },
934   /**
935    * Has the user ever used the Migration Wizard to migrate passwords from
936    * a CSV file?
937    * @return {boolean} `true` if CSV passwords have been imported via the
938    *   migration wizard.
939    */
940   get hasMigratedCSVPasswords() {
941     return lazy.hasMigratedCSVPasswords;
942   },
944   /**
945    * Has the user ever used the Migration Wizard to migrate history?
946    * @return {boolean} `true` if history migration has occurred.
947    */
948   get hasMigratedHistory() {
949     return lazy.hasMigratedHistory;
950   },
952   /**
953    * Has the user ever used the Migration Wizard to migrate passwords?
954    * @return {boolean} `true` if password migration has occurred.
955    */
956   get hasMigratedPasswords() {
957     return lazy.hasMigratedPasswords;
958   },
960   /**
961    * Returns true if the user is configured to use the embedded migration
962    * wizard in about:welcome by having
963    * "browser.migrate.content-modal.about-welcome-behavior" be equal to
964    * "embedded".
965    * @return {boolean} `true` if the embedded migration wizard is enabled.
966    */
967   get useEmbeddedMigrationWizard() {
968     return lazy.useEmbeddedMigrationWizard;
969   },
971   /**
972    * Whether the user installed Firefox via the RTAMO flow.
973    * @return {boolean} `true` when RTAMO has been used to download Firefox,
974    * `false` otherwise.
975    */
976   get isRTAMO() {
977     const { attributionData } = this;
979     return (
980       attributionData?.source === "addons.mozilla.org" &&
981       !!decodeAttributionValue(attributionData?.content)?.startsWith("rta:")
982     );
983   },
985   /**
986    * Whether the user installed via the device migration flow.
987    * @return {boolean} `true` when the link to download the browser was part
988    * of guidance for device migration. `false` otherwise.
989    */
990   get isDeviceMigration() {
991     const { attributionData } = this;
993     return attributionData?.campaign === "migration";
994   },
996   /**
997    * The values of the height and width available to the browser to display
998    * web content. The available height and width are each calculated taking
999    * into account the presence of menu bars, docks, and other similar OS elements
1000    * @returns {Object} resolution The resolution object containing width and height
1001    * @returns {string} resolution.width The available width of the primary monitor
1002    * @returns {string} resolution.height The available height of the primary monitor
1003    */
1004   get primaryResolution() {
1005     // Using hidden dom window ensures that we have a window object
1006     // to grab a screen from in certain edge cases such as targeting evaluation
1007     // during first startup before the browser is available, and in MacOS
1008     let window = Services.appShell.hiddenDOMWindow;
1009     return {
1010       width: window?.screen.availWidth,
1011       height: window?.screen.availHeight,
1012     };
1013   },
1015   get archBits() {
1016     return AppConstants.archBits;
1017   },
1020 const ASRouterTargeting = {
1021   Environment: TargetingGetters,
1023   /**
1024    * Snapshot the current targeting environment.
1025    *
1026    * Asynchronous getters are handled.  Getters that throw or reject
1027    * are ignored.
1028    *
1029    * @param {object} target - the environment to snapshot.
1030    * @return {object} snapshot of target with `environment` object and `version`
1031    * integer.
1032    */
1033   async getEnvironmentSnapshot(target = ASRouterTargeting.Environment) {
1034     async function resolve(object) {
1035       if (typeof object === "object" && object !== null) {
1036         if (Array.isArray(object)) {
1037           return Promise.all(object.map(async item => resolve(await item)));
1038         }
1040         if (object instanceof Date) {
1041           return object;
1042         }
1044         // One promise for each named property. Label promises with property name.
1045         const promises = Object.keys(object).map(async key => {
1046           // Each promise needs to check if we're shutting down when it is evaluated.
1047           if (Services.startup.shuttingDown) {
1048             throw new Error(
1049               "shutting down, so not querying targeting environment"
1050             );
1051           }
1053           const value = await resolve(await object[key]);
1055           return [key, value];
1056         });
1058         const resolved = {};
1059         for (const result of await Promise.allSettled(promises)) {
1060           // Ignore properties that are rejected.
1061           if (result.status === "fulfilled") {
1062             const [key, value] = result.value;
1063             resolved[key] = value;
1064           }
1065         }
1067         return resolved;
1068       }
1070       return object;
1071     }
1073     const environment = await resolve(target);
1075     // Should we need to migrate in the future.
1076     const snapshot = { environment, version: 1 };
1078     return snapshot;
1079   },
1081   isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) {
1082     if (trigger.id !== candidateMessageTrigger.id) {
1083       return false;
1084     } else if (
1085       !candidateMessageTrigger.params &&
1086       !candidateMessageTrigger.patterns
1087     ) {
1088       return true;
1089     }
1091     if (!trigger.param) {
1092       return false;
1093     }
1095     return (
1096       (candidateMessageTrigger.params &&
1097         trigger.param.host &&
1098         candidateMessageTrigger.params.includes(trigger.param.host)) ||
1099       (candidateMessageTrigger.params &&
1100         trigger.param.type &&
1101         candidateMessageTrigger.params.filter(t => t === trigger.param.type)
1102           .length) ||
1103       (candidateMessageTrigger.params &&
1104         trigger.param.type &&
1105         candidateMessageTrigger.params.filter(
1106           t => (t & trigger.param.type) === t
1107         ).length) ||
1108       (candidateMessageTrigger.patterns &&
1109         trigger.param.url &&
1110         new MatchPatternSet(candidateMessageTrigger.patterns).matches(
1111           trigger.param.url
1112         ))
1113     );
1114   },
1116   /**
1117    * getCachedEvaluation - Return a cached jexl evaluation if available
1118    *
1119    * @param {string} targeting JEXL expression to lookup
1120    * @returns {obj|null} Object with value result or null if not available
1121    */
1122   getCachedEvaluation(targeting) {
1123     if (jexlEvaluationCache.has(targeting)) {
1124       const { timestamp, value } = jexlEvaluationCache.get(targeting);
1125       if (Date.now() - timestamp <= CACHE_EXPIRATION) {
1126         return { value };
1127       }
1128       jexlEvaluationCache.delete(targeting);
1129     }
1131     return null;
1132   },
1134   /**
1135    * checkMessageTargeting - Checks is a message's targeting parameters are satisfied
1136    *
1137    * @param {*} message An AS router message
1138    * @param {obj} targetingContext a TargetingContext instance complete with eval environment
1139    * @param {func} onError A function to handle errors (takes two params; error, message)
1140    * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused.
1141    * @returns
1142    */
1143   async checkMessageTargeting(message, targetingContext, onError, shouldCache) {
1144     lazy.ASRouterPreferences.console.debug(
1145       "in checkMessageTargeting, arguments = ",
1146       Array.from(arguments) // eslint-disable-line prefer-rest-params
1147     );
1149     // If no targeting is specified,
1150     if (!message.targeting) {
1151       return true;
1152     }
1153     let result;
1154     try {
1155       if (shouldCache) {
1156         result = this.getCachedEvaluation(message.targeting);
1157         if (result) {
1158           return result.value;
1159         }
1160       }
1161       // Used to report the source of the targeting error in the case of
1162       // undesired events
1163       targetingContext.setTelemetrySource(message.id);
1164       result = await targetingContext.evalWithDefault(message.targeting);
1165       if (shouldCache) {
1166         jexlEvaluationCache.set(message.targeting, {
1167           timestamp: Date.now(),
1168           value: result,
1169         });
1170       }
1171     } catch (error) {
1172       if (onError) {
1173         onError(error, message);
1174       }
1175       console.error(error);
1176       result = false;
1177     }
1178     return result;
1179   },
1181   _isMessageMatch(
1182     message,
1183     trigger,
1184     targetingContext,
1185     onError,
1186     shouldCache = false
1187   ) {
1188     return (
1189       message &&
1190       (trigger
1191         ? this.isTriggerMatch(trigger, message.trigger)
1192         : !message.trigger) &&
1193       // If a trigger expression was passed to this function, the message should match it.
1194       // Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time)
1195       this.checkMessageTargeting(
1196         message,
1197         targetingContext,
1198         onError,
1199         shouldCache
1200       )
1201     );
1202   },
1204   /**
1205    * findMatchingMessage - Given an array of messages, returns one message
1206    *                       whos targeting expression evaluates to true
1207    *
1208    * @param {Array<Message>} messages An array of AS router messages
1209    * @param {trigger} string A trigger expression if a message for that trigger is desired
1210    * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
1211    * @param {func} onError A function to handle errors (takes two params; error, message)
1212    * @param {func} ordered An optional param when true sort message by order specified in message
1213    * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused.
1214    * @param {boolean} returnAll Should we return all matching messages, not just the first one found.
1215    * @returns {obj|Array<Message>} If returnAll is false, a single message. If returnAll is true, an array of messages.
1216    */
1217   async findMatchingMessage({
1218     messages,
1219     trigger = {},
1220     context = {},
1221     onError,
1222     ordered = false,
1223     shouldCache = false,
1224     returnAll = false,
1225   }) {
1226     const sortedMessages = getSortedMessages(messages, { ordered });
1227     lazy.ASRouterPreferences.console.debug(
1228       "in findMatchingMessage, sortedMessages = ",
1229       sortedMessages
1230     );
1231     const matching = returnAll ? [] : null;
1232     const targetingContext = new lazy.TargetingContext(
1233       lazy.TargetingContext.combineContexts(
1234         context,
1235         this.Environment,
1236         trigger.context || {}
1237       )
1238     );
1240     const isMatch = candidate =>
1241       this._isMessageMatch(
1242         candidate,
1243         trigger,
1244         targetingContext,
1245         onError,
1246         shouldCache
1247       );
1249     for (const candidate of sortedMessages) {
1250       if (await isMatch(candidate)) {
1251         // If not returnAll, we should return the first message we find that matches.
1252         if (!returnAll) {
1253           return candidate;
1254         }
1256         matching.push(candidate);
1257       }
1258     }
1259     return matching;
1260   },
1263 const EXPORTED_SYMBOLS = [
1264   "ASRouterTargeting",
1265   "QueryCache",
1266   "CachedTargetingGetter",
1267   "getSortedMessages",