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"
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",
40 XPCOMUtils.defineLazyModuleGetters(lazy, {
41 ASRouterPreferences: "resource://activity-stream/lib/ASRouterPreferences.jsm",
44 XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
45 return ChromeUtils.importESModule(
46 "resource://gre/modules/FxAccounts.sys.mjs"
47 ).getFxAccountsSingleton();
50 XPCOMUtils.defineLazyPreferenceGetter(
52 "cfrFeaturesUserPref",
53 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
56 XPCOMUtils.defineLazyPreferenceGetter(
59 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
62 XPCOMUtils.defineLazyPreferenceGetter(
64 "isWhatsNewPanelEnabled",
65 "browser.messaging-system.whatsNewPanel.enabled",
68 XPCOMUtils.defineLazyPreferenceGetter(
70 "hasAccessedFxAPanel",
71 "identity.fxaccounts.toolbar.accessed",
74 XPCOMUtils.defineLazyPreferenceGetter(
76 "clientsDevicesDesktop",
77 "services.sync.clients.devices.desktop",
80 XPCOMUtils.defineLazyPreferenceGetter(
82 "clientsDevicesMobile",
83 "services.sync.clients.devices.mobile",
86 XPCOMUtils.defineLazyPreferenceGetter(
89 "services.sync.numClients",
92 XPCOMUtils.defineLazyPreferenceGetter(
94 "devtoolsSelfXSSCount",
95 "devtools.selfxss.count",
98 XPCOMUtils.defineLazyPreferenceGetter(
104 XPCOMUtils.defineLazyPreferenceGetter(
106 "isXPIInstallEnabled",
110 XPCOMUtils.defineLazyPreferenceGetter(
113 "browser.newtabpage.activity-stream.feeds.snippets",
116 XPCOMUtils.defineLazyPreferenceGetter(
118 "hasMigratedBookmarks",
119 "browser.migrate.interactions.bookmarks",
122 XPCOMUtils.defineLazyPreferenceGetter(
124 "hasMigratedCSVPasswords",
125 "browser.migrate.interactions.csvpasswords",
128 XPCOMUtils.defineLazyPreferenceGetter(
130 "hasMigratedHistory",
131 "browser.migrate.interactions.history",
134 XPCOMUtils.defineLazyPreferenceGetter(
136 "hasMigratedPasswords",
137 "browser.migrate.interactions.passwords",
140 XPCOMUtils.defineLazyPreferenceGetter(
142 "useEmbeddedMigrationWizard",
143 "browser.migrate.content-modal.about-welcome-behavior",
147 return behaviorString === "embedded";
151 XPCOMUtils.defineLazyServiceGetters(lazy, {
152 AUS: ["@mozilla.org/updates/update-service;1", "nsIApplicationUpdateService"],
153 BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
155 "@mozilla.org/tracking-db-service;1",
156 "nsITrackingDBService",
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
180 function CachedTargetingGetter(
183 updateInterval = FRECENT_SITES_UPDATE_INTERVAL,
191 this._lastUpdated = 0;
195 const now = Date.now();
196 if (now - this._lastUpdated >= updateInterval) {
197 this._value = await getter[property](options);
198 this._lastUpdated = now;
205 function CacheListAttachedOAuthClients() {
210 this._lastUpdated = 0;
214 const now = Date.now();
215 if (now - this._lastUpdated >= FXA_ATTACHED_CLIENTS_UPDATE_INTERVAL) {
216 this._value = new Promise(resolve => {
218 .listAttachedOAuthClients()
222 .catch(() => resolve([]));
224 this._lastUpdated = now;
231 function CheckBrowserNeedsUpdate(
232 updateInterval = FRECENT_SITES_UPDATE_INTERVAL
237 // For testing. Avoid update check network call.
239 this._lastUpdated = Date.now();
243 this._lastUpdated = 0;
247 const now = Date.now();
249 !AppConstants.MOZ_UPDATER ||
250 now - this._lastUpdated < updateInterval
254 if (!lazy.AUS.canCheckForUpdates) {
257 this._lastUpdated = now;
258 let check = lazy.UpdateCheckSvc.checkForUpdates(
259 lazy.UpdateCheckSvc.FOREGROUND_CHECK
261 let result = await check.result;
262 if (!result.succeeded) {
263 lazy.ASRouterPreferences.console.error(
264 "CheckBrowserNeedsUpdate failed :>> ",
269 checker._value = !!result.updates.length;
270 return checker._value;
279 Object.keys(this.queries).forEach(query => {
280 this.queries[query].expire();
282 Object.keys(this.getters).forEach(key => {
283 this.getters[key].expire();
287 TopFrecentSites: new CachedTargetingGetter("getTopFrecentSites", {
288 ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
289 numItems: FRECENT_SITES_NUM_ITEMS,
290 topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
292 includeFavicon: false,
294 TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"),
295 CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(),
296 RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"),
297 ListAttachedOAuthClients: new CacheListAttachedOAuthClients(),
298 UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"),
301 doesAppNeedPin: new CachedTargetingGetter(
304 FRECENT_SITES_UPDATE_INTERVAL,
307 doesAppNeedPrivatePin: new CachedTargetingGetter(
310 FRECENT_SITES_UPDATE_INTERVAL,
313 isDefaultBrowser: new CachedTargetingGetter(
316 FRECENT_SITES_UPDATE_INTERVAL,
319 currentThemes: new CachedTargetingGetter(
322 FRECENT_SITES_UPDATE_INTERVAL,
323 lazy.AddonManager // eslint-disable-line mozilla/valid-lazy
325 isDefaultHTMLHandler: new CachedTargetingGetter(
326 "isDefaultHandlerFor",
328 FRECENT_SITES_UPDATE_INTERVAL,
331 isDefaultPDFHandler: new CachedTargetingGetter(
332 "isDefaultHandlerFor",
334 FRECENT_SITES_UPDATE_INTERVAL,
337 defaultPDFHandler: new CachedTargetingGetter(
338 "getDefaultPDFHandler",
340 FRECENT_SITES_UPDATE_INTERVAL,
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
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
365 function sortMessagesByWeightedRank(messages) {
369 rank: Math.pow(Math.random(), 1 / message.weight),
371 .sort((a, b) => b.rank - a.rank)
372 .map(({ message }) => message);
376 * getSortedMessages - Given an array of Messages, applies sorting and filtering rules
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>}
384 function getSortedMessages(messages, options = {}) {
385 let { ordered } = { ordered: false, ...options };
386 let result = messages;
389 result = sortMessagesByWeightedRank(result);
392 result.sort((a, b) => {
393 // Next, sort by priority
394 if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) {
397 if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) {
401 // Sort messages with targeting expressions higher than those with none
402 if (a.targeting && !b.targeting) {
405 if (!a.targeting && b.targeting) {
409 // Next, sort by order *ascending* if ordered = true
411 if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) {
414 if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) {
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} {
432 * isCustomUrl: boolean,
433 * urls: Array<{url: string, host: string}>
436 function parseAboutPageURL(url) {
442 if (url.startsWith("moz-extension://")) {
444 ret.urls.push({ url, host: "" });
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;
454 const parsedURL = new URL(_url);
455 const host = parsedURL.hostname.replace(/^www\./i, "");
456 ret.urls.push({ url: _url, host });
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: "" });
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
481 async function getAutofillRecords(data) {
484 const win = Services.wm.getMostRecentBrowserWindow();
486 win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
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.
495 let records = await actor?.receiveMessage({
496 name: "FormAutofill:GetRecords",
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) {
509 let decodedValue = value;
511 while (decodedValue.includes("%")) {
513 const result = decodeURIComponent(decodedValue);
514 if (result === decodedValue) {
517 decodedValue = result;
526 const TargetingGetters = {
528 return Services.locale.appLocaleAsBCP47;
530 get localeLanguageCode() {
532 Services.locale.appLocaleAsBCP47 &&
533 Services.locale.appLocaleAsBCP47.substr(0, 2)
536 get browserSettings() {
537 const { settings } = lazy.TelemetryEnvironment.currentEnvironment;
539 update: settings.update,
542 get attributionData() {
543 // Attribution is determined at startup - so we can use the cached attribution at this point
544 return lazy.AttributionCode.getCachedAttributionData();
549 get profileAgeCreated() {
550 return lazy.ProfileAge().then(times => times.created);
552 get profileAgeReset() {
553 return lazy.ProfileAge().then(times => times.reset);
555 get usesFirefoxSync() {
556 return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
559 return lazy.isFxAEnabled;
561 get isFxASignedIn() {
562 return new Promise(resolve => {
563 if (!lazy.isFxAEnabled) {
566 if (Services.prefs.getStringPref(FXA_USERNAME_PREF, "")) {
571 .then(data => resolve(!!data))
572 .catch(e => resolve(false));
577 desktopDevices: lazy.clientsDevicesDesktop,
578 mobileDevices: lazy.clientsDevicesMobile,
579 totalDevices: lazy.syncNumClients,
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;
587 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
588 Ci.nsIBackgroundTasks
590 if (bts?.isBackgroundTaskMode) {
591 return { addons: {}, isFullData: true };
594 return lazy.AddonManager.getActiveAddons(["extension", "service"]).then(
595 ({ addons, fullData }) => {
597 for (const addon of addons) {
599 version: addon.version,
601 isSystem: addon.isSystem,
602 isWebExtension: addon.isWebExtension,
605 Object.assign(info[addon.id], {
607 userDisabled: addon.userDisabled,
608 installDate: addon.installDate,
612 return { addons: info, isFullData: fullData };
616 get searchEngines() {
617 const NONE = { installed: [], current: "" };
618 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
619 Ci.nsIBackgroundTasks
621 if (bts?.isBackgroundTaskMode) {
622 return Promise.resolve(NONE);
624 return new Promise(resolve => {
625 // Note: calling init ensures this code is only executed after Search has been initialized
627 .getAppProvidedEngines()
630 current: Services.search.defaultEngine.identifier,
631 installed: engines.map(engine => engine.identifier),
634 .catch(() => resolve(NONE));
637 get isDefaultBrowser() {
638 return QueryCache.getters.isDefaultBrowser.get().catch(() => null);
640 get devToolsOpenedCount() {
641 return lazy.devtoolsSelfXSSCount;
643 get topFrecentSites() {
644 return QueryCache.queries.TopFrecentSites.get().then(sites =>
647 host: new URL(site.url).hostname,
648 frecency: site.frecency,
649 lastVisitDate: site.lastVisitDate,
653 get recentBookmarks() {
654 return QueryCache.queries.RecentBookmarks.get();
657 return NewTabUtils.pinnedLinks.links.map(site =>
661 host: new URL(site.url).hostname,
662 searchTopSite: site.searchTopSite,
667 get providerCohorts() {
668 return lazy.ASRouterPreferences.providers.reduce((prev, current) => {
669 prev[current.id] = current.cohort || "";
673 get totalBookmarksCount() {
674 return QueryCache.queries.TotalBookmarksCount.get();
676 get firefoxVersion() {
677 return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10);
680 return lazy.Region.home || "";
683 return QueryCache.queries.CheckBrowserNeedsUpdate.get();
685 get hasPinnedTabs() {
686 for (let win of Services.wm.getEnumerator("navigator:browser")) {
687 if (win.closed || !win.ownerGlobal.gBrowser) {
690 if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) {
697 get hasAccessedFxAPanel() {
698 return lazy.hasAccessedFxAPanel;
700 get isWhatsNewPanelEnabled() {
701 return lazy.isWhatsNewPanelEnabled;
705 cfrFeatures: lazy.cfrFeaturesUserPref,
706 cfrAddons: lazy.cfrAddonsUserPref,
707 snippets: lazy.snippetsUserPref,
710 get totalBlockedCount() {
711 return lazy.TrackingDBService.sumAllEvents();
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"],
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(
726 let totalEvents = {};
727 for (let blockedType of idToTextMap.values()) {
728 totalEvents[blockedType] = 0;
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;
740 get attachedFxAOAuthClients() {
741 return this.usesFirefoxSync
742 ? QueryCache.queries.ListAttachedOAuthClients.get()
746 return AppConstants.platform;
748 get isChinaRepack() {
751 .getDefaultBranch(null)
752 .getCharPref(DISTRIBUTION_ID_PREF, "default") ===
753 DISTRIBUTION_ID_CHINA_REPACK
757 return lazy.ClientEnvironment.userId;
759 get profileRestartCount() {
760 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
761 Ci.nsIBackgroundTasks
763 if (bts?.isBackgroundTaskMode) {
766 // Counter starts at 1 when a profile is created, substract 1 so the value
767 // returned matches expectations
769 lazy.TelemetrySession.getMetadata("targeting").profileSubsessionCounter -
773 get homePageSettings() {
774 const url = lazy.HomePage.get();
775 const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
781 isDefault: lazy.HomePage.isDefault,
782 isLocked: lazy.HomePage.locked,
785 get newtabSettings() {
786 const url = lazy.AboutNewTab.newTabURL;
787 const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
792 isDefault: lazy.AboutNewTab.activityStreamEnabled,
797 get activeNotifications() {
798 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
799 Ci.nsIBackgroundTasks
801 if (bts?.isBackgroundTaskMode) {
802 // This might need to hook into the alert service to enumerate relevant
803 // persistent native notifications.
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
816 window.gURLBar?.view.isOpen ||
817 window.gNotificationBox?.currentNotification ||
818 window.gBrowser.getNotificationBox()?.currentNotification
826 get isMajorUpgrade() {
827 return lazy.BrowserHandler.majorUpgrade;
830 get hasActiveEnterprisePolicies() {
831 return Services.policies.status === Services.policies.ACTIVE;
834 get userMonthlyActivity() {
835 return QueryCache.queries.UserMonthlyActivity.get();
838 get doesAppNeedPin() {
839 return QueryCache.getters.doesAppNeedPin.get();
842 get doesAppNeedPrivatePin() {
843 return QueryCache.getters.doesAppNeedPrivatePin.get();
847 * Is this invocation running in background task mode?
849 * @return {boolean} `true` if running in background task mode.
851 get isBackgroundTaskMode() {
852 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
853 Ci.nsIBackgroundTasks
855 return !!bts?.isBackgroundTaskMode;
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.
863 * @return {string|null} background task name or `null`.
865 get backgroundTaskName() {
866 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
867 Ci.nsIBackgroundTasks
869 return bts?.backgroundTaskName();
872 get userPrefersReducedMotion() {
873 let window = Services.appShell.hiddenDOMWindow;
874 return window?.matchMedia("(prefers-reduced-motion: reduce)")?.matches;
878 * Whether or not the user is in the Major Release 2022 holdback study.
880 get inMr2022Holdback() {
882 lazy.NimbusFeatures.majorRelease2022.getVariable("onboarding") === false
887 * The distribution id, if any.
890 get distributionId() {
891 return Services.prefs
892 .getDefaultBranch(null)
893 .getCharPref("distribution.id", "");
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
900 get fxViewButtonAreaType() {
901 let button = lazy.CustomizableUI.getWidget("firefox-view-button");
902 return button.areaType;
907 return QueryCache.getters.isDefaultHTMLHandler.get();
910 return QueryCache.getters.isDefaultPDFHandler.get();
914 get defaultPDFHandler() {
915 return QueryCache.getters.defaultPDFHandler.get();
918 get creditCardsSaved() {
919 return getAutofillRecords({ collectionName: "creditCards" });
922 get addressesSaved() {
923 return getAutofillRecords({ collectionName: "addresses" });
927 * Has the user ever used the Migration Wizard to migrate bookmarks?
928 * @return {boolean} `true` if bookmark migration has occurred.
930 get hasMigratedBookmarks() {
931 return lazy.hasMigratedBookmarks;
935 * Has the user ever used the Migration Wizard to migrate passwords from
937 * @return {boolean} `true` if CSV passwords have been imported via the
940 get hasMigratedCSVPasswords() {
941 return lazy.hasMigratedCSVPasswords;
945 * Has the user ever used the Migration Wizard to migrate history?
946 * @return {boolean} `true` if history migration has occurred.
948 get hasMigratedHistory() {
949 return lazy.hasMigratedHistory;
953 * Has the user ever used the Migration Wizard to migrate passwords?
954 * @return {boolean} `true` if password migration has occurred.
956 get hasMigratedPasswords() {
957 return lazy.hasMigratedPasswords;
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
965 * @return {boolean} `true` if the embedded migration wizard is enabled.
967 get useEmbeddedMigrationWizard() {
968 return lazy.useEmbeddedMigrationWizard;
972 * Whether the user installed Firefox via the RTAMO flow.
973 * @return {boolean} `true` when RTAMO has been used to download Firefox,
977 const { attributionData } = this;
980 attributionData?.source === "addons.mozilla.org" &&
981 !!decodeAttributionValue(attributionData?.content)?.startsWith("rta:")
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.
990 get isDeviceMigration() {
991 const { attributionData } = this;
993 return attributionData?.campaign === "migration";
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
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;
1010 width: window?.screen.availWidth,
1011 height: window?.screen.availHeight,
1016 return AppConstants.archBits;
1020 const ASRouterTargeting = {
1021 Environment: TargetingGetters,
1024 * Snapshot the current targeting environment.
1026 * Asynchronous getters are handled. Getters that throw or reject
1029 * @param {object} target - the environment to snapshot.
1030 * @return {object} snapshot of target with `environment` object and `version`
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)));
1040 if (object instanceof Date) {
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) {
1049 "shutting down, so not querying targeting environment"
1053 const value = await resolve(await object[key]);
1055 return [key, value];
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;
1073 const environment = await resolve(target);
1075 // Should we need to migrate in the future.
1076 const snapshot = { environment, version: 1 };
1081 isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) {
1082 if (trigger.id !== candidateMessageTrigger.id) {
1085 !candidateMessageTrigger.params &&
1086 !candidateMessageTrigger.patterns
1091 if (!trigger.param) {
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)
1103 (candidateMessageTrigger.params &&
1104 trigger.param.type &&
1105 candidateMessageTrigger.params.filter(
1106 t => (t & trigger.param.type) === t
1108 (candidateMessageTrigger.patterns &&
1109 trigger.param.url &&
1110 new MatchPatternSet(candidateMessageTrigger.patterns).matches(
1117 * getCachedEvaluation - Return a cached jexl evaluation if available
1119 * @param {string} targeting JEXL expression to lookup
1120 * @returns {obj|null} Object with value result or null if not available
1122 getCachedEvaluation(targeting) {
1123 if (jexlEvaluationCache.has(targeting)) {
1124 const { timestamp, value } = jexlEvaluationCache.get(targeting);
1125 if (Date.now() - timestamp <= CACHE_EXPIRATION) {
1128 jexlEvaluationCache.delete(targeting);
1135 * checkMessageTargeting - Checks is a message's targeting parameters are satisfied
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.
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
1149 // If no targeting is specified,
1150 if (!message.targeting) {
1156 result = this.getCachedEvaluation(message.targeting);
1158 return result.value;
1161 // Used to report the source of the targeting error in the case of
1163 targetingContext.setTelemetrySource(message.id);
1164 result = await targetingContext.evalWithDefault(message.targeting);
1166 jexlEvaluationCache.set(message.targeting, {
1167 timestamp: Date.now(),
1173 onError(error, message);
1175 console.error(error);
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(
1205 * findMatchingMessage - Given an array of messages, returns one message
1206 * whos targeting expression evaluates to true
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.
1217 async findMatchingMessage({
1223 shouldCache = false,
1226 const sortedMessages = getSortedMessages(messages, { ordered });
1227 lazy.ASRouterPreferences.console.debug(
1228 "in findMatchingMessage, sortedMessages = ",
1231 const matching = returnAll ? [] : null;
1232 const targetingContext = new lazy.TargetingContext(
1233 lazy.TargetingContext.combineContexts(
1236 trigger.context || {}
1240 const isMatch = candidate =>
1241 this._isMessageMatch(
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.
1256 matching.push(candidate);
1263 const EXPORTED_SYMBOLS = [
1264 "ASRouterTargeting",
1266 "CachedTargetingGetter",
1267 "getSortedMessages",