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"
38 ChromeUtils.defineESModuleGetters(lazy, {
39 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
40 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
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",
57 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
58 return ChromeUtils.importESModule(
59 "resource://gre/modules/FxAccounts.sys.mjs"
60 ).getFxAccountsSingleton();
63 XPCOMUtils.defineLazyPreferenceGetter(
65 "cfrFeaturesUserPref",
66 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
69 XPCOMUtils.defineLazyPreferenceGetter(
72 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
75 XPCOMUtils.defineLazyPreferenceGetter(
77 "isWhatsNewPanelEnabled",
78 "browser.messaging-system.whatsNewPanel.enabled",
81 XPCOMUtils.defineLazyPreferenceGetter(
83 "hasAccessedFxAPanel",
84 "identity.fxaccounts.toolbar.accessed",
87 XPCOMUtils.defineLazyPreferenceGetter(
89 "clientsDevicesDesktop",
90 "services.sync.clients.devices.desktop",
93 XPCOMUtils.defineLazyPreferenceGetter(
95 "clientsDevicesMobile",
96 "services.sync.clients.devices.mobile",
99 XPCOMUtils.defineLazyPreferenceGetter(
102 "services.sync.numClients",
105 XPCOMUtils.defineLazyPreferenceGetter(
107 "devtoolsSelfXSSCount",
108 "devtools.selfxss.count",
111 XPCOMUtils.defineLazyPreferenceGetter(
117 XPCOMUtils.defineLazyPreferenceGetter(
119 "isXPIInstallEnabled",
123 XPCOMUtils.defineLazyPreferenceGetter(
125 "hasMigratedBookmarks",
126 "browser.migrate.interactions.bookmarks",
129 XPCOMUtils.defineLazyPreferenceGetter(
131 "hasMigratedCSVPasswords",
132 "browser.migrate.interactions.csvpasswords",
135 XPCOMUtils.defineLazyPreferenceGetter(
137 "hasMigratedHistory",
138 "browser.migrate.interactions.history",
141 XPCOMUtils.defineLazyPreferenceGetter(
143 "hasMigratedPasswords",
144 "browser.migrate.interactions.passwords",
147 XPCOMUtils.defineLazyPreferenceGetter(
149 "useEmbeddedMigrationWizard",
150 "browser.migrate.content-modal.about-welcome-behavior",
154 return behaviorString === "embedded";
158 XPCOMUtils.defineLazyServiceGetters(lazy, {
159 AUS: ["@mozilla.org/updates/update-service;1", "nsIApplicationUpdateService"],
160 BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
162 "@mozilla.org/tracking-db-service;1",
163 "nsITrackingDBService",
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
187 export function CachedTargetingGetter(
190 updateInterval = FRECENT_SITES_UPDATE_INTERVAL,
198 this._lastUpdated = 0;
202 const now = Date.now();
203 if (now - this._lastUpdated >= updateInterval) {
204 this._value = await getter[property](options);
205 this._lastUpdated = now;
212 function CacheListAttachedOAuthClients() {
217 this._lastUpdated = 0;
221 const now = Date.now();
222 if (now - this._lastUpdated >= FXA_ATTACHED_CLIENTS_UPDATE_INTERVAL) {
223 this._value = new Promise(resolve => {
225 .listAttachedOAuthClients()
229 .catch(() => resolve([]));
231 this._lastUpdated = now;
238 function CheckBrowserNeedsUpdate(
239 updateInterval = FRECENT_SITES_UPDATE_INTERVAL
244 // For testing. Avoid update check network call.
246 this._lastUpdated = Date.now();
250 this._lastUpdated = 0;
254 const now = Date.now();
256 !AppConstants.MOZ_UPDATER ||
257 now - this._lastUpdated < updateInterval
261 if (!lazy.AUS.canCheckForUpdates) {
264 this._lastUpdated = now;
265 let check = lazy.UpdateCheckSvc.checkForUpdates(
266 lazy.UpdateCheckSvc.FOREGROUND_CHECK
268 let result = await check.result;
269 if (!result.succeeded) {
270 lazy.ASRouterPreferences.console.error(
271 "CheckBrowserNeedsUpdate failed :>> ",
276 checker._value = !!result.updates.length;
277 return checker._value;
284 export const QueryCache = {
286 Object.keys(this.queries).forEach(query => {
287 this.queries[query].expire();
289 Object.keys(this.getters).forEach(key => {
290 this.getters[key].expire();
294 TopFrecentSites: new CachedTargetingGetter("getTopFrecentSites", {
295 ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
296 numItems: FRECENT_SITES_NUM_ITEMS,
297 topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
299 includeFavicon: false,
301 TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"),
302 CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(),
303 RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"),
304 ListAttachedOAuthClients: new CacheListAttachedOAuthClients(),
305 UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"),
308 doesAppNeedPin: new CachedTargetingGetter(
311 FRECENT_SITES_UPDATE_INTERVAL,
314 doesAppNeedPrivatePin: new CachedTargetingGetter(
317 FRECENT_SITES_UPDATE_INTERVAL,
320 isDefaultBrowser: new CachedTargetingGetter(
323 FRECENT_SITES_UPDATE_INTERVAL,
326 currentThemes: new CachedTargetingGetter(
329 FRECENT_SITES_UPDATE_INTERVAL,
330 lazy.AddonManager // eslint-disable-line mozilla/valid-lazy
332 isDefaultHTMLHandler: new CachedTargetingGetter(
333 "isDefaultHandlerFor",
335 FRECENT_SITES_UPDATE_INTERVAL,
338 isDefaultPDFHandler: new CachedTargetingGetter(
339 "isDefaultHandlerFor",
341 FRECENT_SITES_UPDATE_INTERVAL,
344 defaultPDFHandler: new CachedTargetingGetter(
345 "getDefaultPDFHandler",
347 FRECENT_SITES_UPDATE_INTERVAL,
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
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
372 function sortMessagesByWeightedRank(messages) {
376 rank: Math.pow(Math.random(), 1 / message.weight),
378 .sort((a, b) => b.rank - a.rank)
379 .map(({ message }) => message);
383 * getSortedMessages - Given an array of Messages, applies sorting and filtering rules
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>}
391 export function getSortedMessages(messages, options = {}) {
392 let { ordered } = { ordered: false, ...options };
393 let result = messages;
396 result = sortMessagesByWeightedRank(result);
399 result.sort((a, b) => {
400 // Next, sort by priority
401 if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) {
404 if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) {
408 // Sort messages with targeting expressions higher than those with none
409 if (a.targeting && !b.targeting) {
412 if (!a.targeting && b.targeting) {
416 // Next, sort by order *ascending* if ordered = true
418 if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) {
421 if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) {
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} {
439 * isCustomUrl: boolean,
440 * urls: Array<{url: string, host: string}>
443 function parseAboutPageURL(url) {
449 if (url.startsWith("moz-extension://")) {
451 ret.urls.push({ url, host: "" });
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;
461 const parsedURL = new URL(_url);
462 const host = parsedURL.hostname.replace(/^www\./i, "");
463 ret.urls.push({ url: _url, host });
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: "" });
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
488 async function getAutofillRecords(data) {
491 const win = Services.wm.getMostRecentBrowserWindow();
493 win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
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.
502 let records = await actor?.receiveMessage({
503 name: "FormAutofill:GetRecords",
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) {
516 let decodedValue = value;
518 while (decodedValue.includes("%")) {
520 const result = decodeURIComponent(decodedValue);
521 if (result === decodedValue) {
524 decodedValue = result;
533 const TargetingGetters = {
535 return Services.locale.appLocaleAsBCP47;
537 get localeLanguageCode() {
539 Services.locale.appLocaleAsBCP47 &&
540 Services.locale.appLocaleAsBCP47.substr(0, 2)
543 get browserSettings() {
544 const { settings } = lazy.TelemetryEnvironment.currentEnvironment;
546 update: settings.update,
549 get attributionData() {
550 // Attribution is determined at startup - so we can use the cached attribution at this point
551 return lazy.AttributionCode.getCachedAttributionData();
556 get profileAgeCreated() {
557 return lazy.ProfileAge().then(times => times.created);
559 get profileAgeReset() {
560 return lazy.ProfileAge().then(times => times.reset);
562 get usesFirefoxSync() {
563 return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
566 return lazy.isFxAEnabled;
568 get isFxASignedIn() {
569 return new Promise(resolve => {
570 if (!lazy.isFxAEnabled) {
573 if (Services.prefs.getStringPref(FXA_USERNAME_PREF, "")) {
578 .then(data => resolve(!!data))
579 .catch(e => resolve(false));
584 desktopDevices: lazy.clientsDevicesDesktop,
585 mobileDevices: lazy.clientsDevicesMobile,
586 totalDevices: lazy.syncNumClients,
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;
594 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
595 Ci.nsIBackgroundTasks
597 if (bts?.isBackgroundTaskMode) {
598 return { addons: {}, isFullData: true };
601 return lazy.AddonManager.getActiveAddons(["extension", "service"]).then(
602 ({ addons, fullData }) => {
604 for (const addon of addons) {
606 version: addon.version,
608 isSystem: addon.isSystem,
609 isWebExtension: addon.isWebExtension,
612 Object.assign(info[addon.id], {
614 userDisabled: addon.userDisabled,
615 installDate: addon.installDate,
619 return { addons: info, isFullData: fullData };
623 get searchEngines() {
624 const NONE = { installed: [], current: "" };
625 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
626 Ci.nsIBackgroundTasks
628 if (bts?.isBackgroundTaskMode) {
629 return Promise.resolve(NONE);
631 return new Promise(resolve => {
632 // Note: calling init ensures this code is only executed after Search has been initialized
634 .getAppProvidedEngines()
637 current: Services.search.defaultEngine.identifier,
638 installed: engines.map(engine => engine.identifier),
641 .catch(() => resolve(NONE));
644 get isDefaultBrowser() {
645 return QueryCache.getters.isDefaultBrowser.get().catch(() => null);
647 get devToolsOpenedCount() {
648 return lazy.devtoolsSelfXSSCount;
650 get topFrecentSites() {
651 return QueryCache.queries.TopFrecentSites.get().then(sites =>
654 host: new URL(site.url).hostname,
655 frecency: site.frecency,
656 lastVisitDate: site.lastVisitDate,
660 get recentBookmarks() {
661 return QueryCache.queries.RecentBookmarks.get();
664 return NewTabUtils.pinnedLinks.links.map(site =>
668 host: new URL(site.url).hostname,
669 searchTopSite: site.searchTopSite,
674 get providerCohorts() {
675 return lazy.ASRouterPreferences.providers.reduce((prev, current) => {
676 prev[current.id] = current.cohort || "";
680 get totalBookmarksCount() {
681 return QueryCache.queries.TotalBookmarksCount.get();
683 get firefoxVersion() {
684 return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10);
687 return lazy.Region.home || "";
690 return QueryCache.queries.CheckBrowserNeedsUpdate.get();
692 get hasPinnedTabs() {
693 for (let win of Services.wm.getEnumerator("navigator:browser")) {
694 if (win.closed || !win.ownerGlobal.gBrowser) {
697 if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) {
704 get hasAccessedFxAPanel() {
705 return lazy.hasAccessedFxAPanel;
707 get isWhatsNewPanelEnabled() {
708 return lazy.isWhatsNewPanelEnabled;
712 cfrFeatures: lazy.cfrFeaturesUserPref,
713 cfrAddons: lazy.cfrAddonsUserPref,
716 get totalBlockedCount() {
717 return lazy.TrackingDBService.sumAllEvents();
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"],
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(
732 let totalEvents = {};
733 for (let blockedType of idToTextMap.values()) {
734 totalEvents[blockedType] = 0;
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;
746 get attachedFxAOAuthClients() {
747 return this.usesFirefoxSync
748 ? QueryCache.queries.ListAttachedOAuthClients.get()
752 return AppConstants.platform;
754 get isChinaRepack() {
757 .getDefaultBranch(null)
758 .getCharPref(DISTRIBUTION_ID_PREF, "default") ===
759 DISTRIBUTION_ID_CHINA_REPACK
763 return lazy.ClientEnvironment.userId;
765 get profileRestartCount() {
766 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
767 Ci.nsIBackgroundTasks
769 if (bts?.isBackgroundTaskMode) {
772 // Counter starts at 1 when a profile is created, substract 1 so the value
773 // returned matches expectations
775 lazy.TelemetrySession.getMetadata("targeting").profileSubsessionCounter -
779 get homePageSettings() {
780 const url = lazy.HomePage.get();
781 const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
787 isDefault: lazy.HomePage.isDefault,
788 isLocked: lazy.HomePage.locked,
791 get newtabSettings() {
792 const url = lazy.AboutNewTab.newTabURL;
793 const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url);
798 isDefault: lazy.AboutNewTab.activityStreamEnabled,
803 get activeNotifications() {
804 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
805 Ci.nsIBackgroundTasks
807 if (bts?.isBackgroundTaskMode) {
808 // This might need to hook into the alert service to enumerate relevant
809 // persistent native notifications.
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
822 window.gURLBar?.view.isOpen ||
823 window.gNotificationBox?.currentNotification ||
824 window.gBrowser.getNotificationBox()?.currentNotification
832 get isMajorUpgrade() {
833 return lazy.BrowserHandler.majorUpgrade;
836 get hasActiveEnterprisePolicies() {
837 return Services.policies.status === Services.policies.ACTIVE;
840 get userMonthlyActivity() {
841 return QueryCache.queries.UserMonthlyActivity.get();
844 get doesAppNeedPin() {
845 return QueryCache.getters.doesAppNeedPin.get();
848 get doesAppNeedPrivatePin() {
849 return QueryCache.getters.doesAppNeedPrivatePin.get();
852 get launchOnLoginEnabled() {
853 if (AppConstants.platform !== "win") {
856 return lazy.WindowsLaunchOnLogin.getLaunchOnLoginEnabled();
860 * Is this invocation running in background task mode?
862 * @return {boolean} `true` if running in background task mode.
864 get isBackgroundTaskMode() {
865 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
866 Ci.nsIBackgroundTasks
868 return !!bts?.isBackgroundTaskMode;
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.
876 * @return {string|null} background task name or `null`.
878 get backgroundTaskName() {
879 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
880 Ci.nsIBackgroundTasks
882 return bts?.backgroundTaskName();
885 get userPrefersReducedMotion() {
886 let window = Services.appShell.hiddenDOMWindow;
887 return window?.matchMedia("(prefers-reduced-motion: reduce)")?.matches;
891 * Whether or not the user is in the Major Release 2022 holdback study.
893 get inMr2022Holdback() {
895 lazy.NimbusFeatures.majorRelease2022.getVariable("onboarding") === false
900 * The distribution id, if any.
903 get distributionId() {
904 return Services.prefs
905 .getDefaultBranch(null)
906 .getCharPref("distribution.id", "");
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
913 get fxViewButtonAreaType() {
914 let button = lazy.CustomizableUI.getWidget("firefox-view-button");
915 return button.areaType;
920 return QueryCache.getters.isDefaultHTMLHandler.get();
923 return QueryCache.getters.isDefaultPDFHandler.get();
927 get defaultPDFHandler() {
928 return QueryCache.getters.defaultPDFHandler.get();
931 get creditCardsSaved() {
932 return getAutofillRecords({ collectionName: "creditCards" });
935 get addressesSaved() {
936 return getAutofillRecords({ collectionName: "addresses" });
940 * Has the user ever used the Migration Wizard to migrate bookmarks?
941 * @return {boolean} `true` if bookmark migration has occurred.
943 get hasMigratedBookmarks() {
944 return lazy.hasMigratedBookmarks;
948 * Has the user ever used the Migration Wizard to migrate passwords from
950 * @return {boolean} `true` if CSV passwords have been imported via the
953 get hasMigratedCSVPasswords() {
954 return lazy.hasMigratedCSVPasswords;
958 * Has the user ever used the Migration Wizard to migrate history?
959 * @return {boolean} `true` if history migration has occurred.
961 get hasMigratedHistory() {
962 return lazy.hasMigratedHistory;
966 * Has the user ever used the Migration Wizard to migrate passwords?
967 * @return {boolean} `true` if password migration has occurred.
969 get hasMigratedPasswords() {
970 return lazy.hasMigratedPasswords;
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
978 * @return {boolean} `true` if the embedded migration wizard is enabled.
980 get useEmbeddedMigrationWizard() {
981 return lazy.useEmbeddedMigrationWizard;
985 * Whether the user installed Firefox via the RTAMO flow.
986 * @return {boolean} `true` when RTAMO has been used to download Firefox,
990 const { attributionData } = this;
993 attributionData?.source === "addons.mozilla.org" &&
994 !!decodeAttributionValue(attributionData?.content)?.startsWith("rta:")
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.
1003 get isDeviceMigration() {
1004 const { attributionData } = this;
1006 return attributionData?.campaign === "migration";
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
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;
1023 width: window?.screen.availWidth,
1024 height: window?.screen.availHeight,
1031 bits = Services.sysinfo.getProperty("archbits", null);
1033 // getProperty can throw if the memsize does not exist
1036 bits = Number(bits);
1044 memory = Services.sysinfo.getProperty("memsize", null);
1046 // getProperty can throw if the memsize does not exist
1049 memory = Number(memory) / 1024 / 1024;
1055 export const ASRouterTargeting = {
1056 Environment: TargetingGetters,
1059 * Snapshot the current targeting environment.
1061 * Asynchronous getters are handled. Getters that throw or reject
1064 * Leftward (earlier) targets supercede rightward (later) targets, just like
1065 * `TargetingContext.combineContexts`.
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.
1072 async getEnvironmentSnapshot({
1073 targets = [ASRouterTargeting.Environment],
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)));
1081 if (object instanceof Date) {
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) {
1090 "shutting down, so not querying targeting environment"
1094 const value = await resolve(await object[key]);
1096 return [key, value];
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;
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));
1122 // Should we need to migrate in the future.
1123 const snapshot = { environment, version: 1 };
1128 isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) {
1129 if (trigger.id !== candidateMessageTrigger.id) {
1132 !candidateMessageTrigger.params &&
1133 !candidateMessageTrigger.patterns
1138 if (!trigger.param) {
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)
1150 (candidateMessageTrigger.params &&
1151 trigger.param.type &&
1152 candidateMessageTrigger.params.filter(
1153 t => (t & trigger.param.type) === t
1155 (candidateMessageTrigger.patterns &&
1156 trigger.param.url &&
1157 new MatchPatternSet(candidateMessageTrigger.patterns).matches(
1164 * getCachedEvaluation - Return a cached jexl evaluation if available
1166 * @param {string} targeting JEXL expression to lookup
1167 * @returns {obj|null} Object with value result or null if not available
1169 getCachedEvaluation(targeting) {
1170 if (jexlEvaluationCache.has(targeting)) {
1171 const { timestamp, value } = jexlEvaluationCache.get(targeting);
1172 if (Date.now() - timestamp <= CACHE_EXPIRATION) {
1175 jexlEvaluationCache.delete(targeting);
1182 * checkMessageTargeting - Checks is a message's targeting parameters are satisfied
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.
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
1196 // If no targeting is specified,
1197 if (!message.targeting) {
1203 result = this.getCachedEvaluation(message.targeting);
1205 return result.value;
1208 // Used to report the source of the targeting error in the case of
1210 targetingContext.setTelemetrySource(message.id);
1211 result = await targetingContext.evalWithDefault(message.targeting);
1213 jexlEvaluationCache.set(message.targeting, {
1214 timestamp: Date.now(),
1220 onError(error, message);
1222 console.error(error);
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(
1252 * findMatchingMessage - Given an array of messages, returns one message
1253 * whos targeting expression evaluates to true
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.
1264 async findMatchingMessage({
1270 shouldCache = false,
1273 const sortedMessages = getSortedMessages(messages, { ordered });
1274 lazy.ASRouterPreferences.console.debug(
1275 "in findMatchingMessage, sortedMessages = ",
1278 const matching = returnAll ? [] : null;
1279 const targetingContext = new lazy.TargetingContext(
1280 lazy.TargetingContext.combineContexts(
1283 trigger.context || {}
1287 const isMatch = candidate =>
1288 this._isMessageMatch(
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.
1303 matching.push(candidate);