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